1#![deny(missing_docs)]
2mod lex;
27mod parse;
28use lazy_regex::regex_captures;
29pub mod changes;
30pub mod textwrap;
31use crate::parse::{SyntaxNode, SyntaxToken};
32use debversion::Version;
33use rowan::ast::AstNode;
34
35pub use crate::changes::changes_by_author;
36pub use crate::parse::{
37 ChangeLog, Entry, EntryBody, EntryFooter, EntryHeader, Error, IntoTimestamp, Maintainer,
38 MetadataEntry, MetadataKey, MetadataValue, Parse, ParseError, Timestamp, Urgency,
39};
40
41#[derive(Debug, Clone)]
46pub struct Change {
47 entry: Entry,
49 author: Option<String>,
51 line_numbers: Vec<usize>,
53 detail_tokens: Vec<SyntaxToken>,
55}
56
57impl Change {
58 pub(crate) fn new(
60 entry: Entry,
61 author: Option<String>,
62 line_numbers: Vec<usize>,
63 detail_tokens: Vec<SyntaxToken>,
64 ) -> Self {
65 Self {
66 entry,
67 author,
68 line_numbers,
69 detail_tokens,
70 }
71 }
72
73 pub fn author(&self) -> Option<&str> {
75 self.author.as_deref()
76 }
77
78 pub fn line_numbers(&self) -> &[usize] {
80 &self.line_numbers
81 }
82
83 pub fn lines(&self) -> Vec<String> {
85 self.detail_tokens
86 .iter()
87 .map(|token| token.text().to_string())
88 .collect()
89 }
90
91 pub fn package(&self) -> Option<String> {
93 self.entry.package()
94 }
95
96 pub fn try_version(&self) -> Option<Result<Version, debversion::ParseError>> {
103 self.entry.try_version()
104 }
105
106 pub fn version(&self) -> Option<Version> {
111 self.try_version().and_then(|r| r.ok())
112 }
113
114 pub fn is_attributed(&self) -> bool {
116 self.author.is_some()
117 }
118
119 pub fn entry(&self) -> &Entry {
121 &self.entry
122 }
123
124 pub fn line(&self) -> Option<usize> {
128 self.detail_tokens.first().map(|token| {
129 parse::line_col_at_offset(self.entry.syntax(), token.text_range().start()).0
130 })
131 }
132
133 pub fn column(&self) -> Option<usize> {
137 self.detail_tokens.first().map(|token| {
138 parse::line_col_at_offset(self.entry.syntax(), token.text_range().start()).1
139 })
140 }
141
142 pub fn line_col(&self) -> Option<(usize, usize)> {
147 self.detail_tokens
148 .first()
149 .map(|token| parse::line_col_at_offset(self.entry.syntax(), token.text_range().start()))
150 }
151
152 pub fn remove(self) {
158 let author = self.author.clone();
160
161 let mut body_nodes_to_remove = Vec::new();
163
164 for token in &self.detail_tokens {
165 if let Some(parent) = token.parent() {
166 if parent.kind() == SyntaxKind::ENTRY_BODY {
167 if !body_nodes_to_remove
169 .iter()
170 .any(|n: &SyntaxNode| n == &parent)
171 {
172 body_nodes_to_remove.push(parent);
173 }
174 }
175 }
176 }
177
178 let section_header_index = if author.is_some() && !body_nodes_to_remove.is_empty() {
181 Self::find_section_header_for_changes(&self.entry, &body_nodes_to_remove)
182 .map(|node| node.index())
183 } else {
184 None
185 };
186
187 let mut sorted_nodes = body_nodes_to_remove;
190 sorted_nodes.sort_by_key(|n| std::cmp::Reverse(n.index()));
191
192 let mut indices_to_remove = Vec::new();
194 let children: Vec<_> = self.entry.syntax().children().collect();
195
196 for body_node in &sorted_nodes {
197 let index = body_node.index();
198 indices_to_remove.push(index);
199
200 if Self::should_remove_trailing_empty(&children, index) {
202 indices_to_remove.push(index + 1);
203 }
204 }
205
206 indices_to_remove.sort_by_key(|&i| std::cmp::Reverse(i));
208 indices_to_remove.dedup();
209
210 for index in indices_to_remove {
212 self.entry
213 .syntax()
214 .splice_children(index..index + 1, vec![]);
215 }
216
217 if let Some(original_header_idx) = section_header_index {
221 let nodes_removed_before_header = sorted_nodes
223 .iter()
224 .filter(|n| n.index() < original_header_idx)
225 .count();
226
227 let adjusted_header_idx = original_header_idx - nodes_removed_before_header;
229
230 Self::remove_section_header_if_empty_at_index(&self.entry, adjusted_header_idx);
231 }
232 }
233
234 fn is_section_header(node: &SyntaxNode) -> bool {
236 if node.kind() != SyntaxKind::ENTRY_BODY {
237 return false;
238 }
239
240 for token in node.descendants_with_tokens() {
241 if let Some(token) = token.as_token() {
242 if token.kind() == SyntaxKind::DETAIL
243 && crate::changes::extract_author_name(token.text()).is_some()
244 {
245 return true;
246 }
247 }
248 }
249
250 false
251 }
252
253 fn should_remove_trailing_empty(children: &[SyntaxNode], entry_index: usize) -> bool {
256 let has_trailing_empty = children
258 .get(entry_index + 1)
259 .is_some_and(|n| n.kind() == SyntaxKind::EMPTY_LINE);
260
261 if !has_trailing_empty {
262 return false;
263 }
264
265 let has_preceding_empty = entry_index > 0
267 && children
268 .get(entry_index - 1)
269 .is_some_and(|n| n.kind() == SyntaxKind::EMPTY_LINE);
270
271 if has_preceding_empty {
272 return true;
273 }
274
275 match children.get(entry_index + 2) {
277 Some(node) if node.kind() == SyntaxKind::EMPTY_LINE => true,
278 Some(node) if Self::is_section_header(node) => true,
279 _ => false,
280 }
281 }
282
283 fn should_remove_preceding_empty(children: &[SyntaxNode], header_index: usize) -> bool {
286 if header_index == 0 {
287 return false;
288 }
289
290 let has_preceding_empty = children
292 .get(header_index - 1)
293 .is_some_and(|n| n.kind() == SyntaxKind::EMPTY_LINE);
294
295 if !has_preceding_empty {
296 return false;
297 }
298
299 let is_first_blank_after_header = header_index >= 2
301 && children
302 .get(header_index - 2)
303 .is_some_and(|n| n.kind() == SyntaxKind::ENTRY_HEADER);
304
305 !is_first_blank_after_header
306 }
307
308 fn find_section_header_for_changes(
310 entry: &Entry,
311 change_nodes: &[SyntaxNode],
312 ) -> Option<SyntaxNode> {
313 if change_nodes.is_empty() {
314 return None;
315 }
316
317 let first_change_index = change_nodes.iter().map(|n| n.index()).min().unwrap();
318 let mut header_node = None;
319
320 for child in entry.syntax().children() {
321 for token_or_node in child.children_with_tokens() {
322 let Some(token) = token_or_node.as_token() else {
323 continue;
324 };
325 if token.kind() != SyntaxKind::DETAIL {
326 continue;
327 }
328
329 let Some(parent) = token.parent() else {
330 continue;
331 };
332 if parent.kind() != SyntaxKind::ENTRY_BODY {
333 continue;
334 }
335
336 let parent_index = parent.index();
337 if parent_index >= first_change_index {
338 continue;
339 }
340
341 if crate::changes::extract_author_name(token.text()).is_some() {
342 header_node = Some(parent);
343 }
344 }
345 }
346
347 header_node
348 }
349
350 fn remove_section_header_if_empty_at_index(entry: &Entry, header_index: usize) {
352 let mut has_bullets_in_section = false;
354
355 'outer: for child in entry.syntax().children() {
356 for token_or_node in child.children_with_tokens() {
357 let Some(token) = token_or_node.as_token() else {
358 continue;
359 };
360 if token.kind() != SyntaxKind::DETAIL {
361 continue;
362 }
363
364 let Some(parent) = token.parent() else {
365 continue;
366 };
367 if parent.kind() != SyntaxKind::ENTRY_BODY {
368 continue;
369 }
370
371 let parent_index = parent.index();
372 if parent_index <= header_index {
373 continue;
374 }
375
376 let text = token.text();
377 if crate::changes::extract_author_name(text).is_some() {
379 break 'outer;
380 }
381
382 if text.starts_with("* ") {
384 has_bullets_in_section = true;
385 break 'outer;
386 }
387 }
388 }
389
390 if !has_bullets_in_section {
392 let children: Vec<_> = entry.syntax().children().collect();
393
394 let start_index = if Self::should_remove_preceding_empty(&children, header_index) {
397 header_index - 1
398 } else {
399 header_index
400 };
401
402 for idx in (start_index..=header_index).rev() {
406 entry.syntax().splice_children(idx..idx + 1, vec![]);
407 }
408 }
409 }
410
411 pub fn replace_with(&self, new_lines: Vec<&str>) {
418 use rowan::GreenNodeBuilder;
419
420 let first_body_node = self
422 .detail_tokens
423 .first()
424 .and_then(|token| token.parent())
425 .filter(|parent| parent.kind() == SyntaxKind::ENTRY_BODY);
426
427 if let Some(_first_node) = first_body_node {
428 let mut body_nodes_to_remove = Vec::new();
430 for token in &self.detail_tokens {
431 if let Some(parent) = token.parent() {
432 if parent.kind() == SyntaxKind::ENTRY_BODY
433 && !body_nodes_to_remove
434 .iter()
435 .any(|n: &SyntaxNode| n == &parent)
436 {
437 body_nodes_to_remove.push(parent);
438 }
439 }
440 }
441
442 let mut new_nodes = Vec::new();
444 for line in new_lines {
445 let mut builder = GreenNodeBuilder::new();
446 builder.start_node(SyntaxKind::ENTRY_BODY.into());
447 if !line.is_empty() {
448 builder.token(SyntaxKind::INDENT.into(), " ");
449 builder.token(SyntaxKind::DETAIL.into(), line);
450 }
451 builder.token(SyntaxKind::NEWLINE.into(), "\n");
452 builder.finish_node();
453
454 let syntax = SyntaxNode::new_root_mut(builder.finish());
455 new_nodes.push(syntax.into());
456 }
457
458 let mut sorted_nodes = body_nodes_to_remove.clone();
461 sorted_nodes.sort_by_key(|n| std::cmp::Reverse(n.index()));
462
463 for (i, node) in sorted_nodes.iter().enumerate() {
464 let idx = node.index();
465 if i == 0 {
466 self.entry
468 .syntax()
469 .splice_children(idx..idx + 1, new_nodes.clone());
470 } else {
471 self.entry.syntax().splice_children(idx..idx + 1, vec![]);
473 }
474 }
475 }
476 }
477
478 pub fn replace_line(&self, index: usize, new_text: &str) -> Result<(), Error> {
506 if index >= self.detail_tokens.len() {
507 return Err(Error::Io(std::io::Error::new(
508 std::io::ErrorKind::InvalidInput,
509 format!(
510 "Line index {} out of bounds (0..{})",
511 index,
512 self.detail_tokens.len()
513 ),
514 )));
515 }
516
517 let mut new_lines = self.lines();
518 new_lines[index] = new_text.to_string();
519
520 self.replace_with(new_lines.iter().map(|s| s.as_str()).collect());
521 Ok(())
522 }
523
524 pub fn update_lines<F, G>(&self, predicate: F, updater: G) -> usize
560 where
561 F: Fn(&str) -> bool,
562 G: Fn(&str) -> String,
563 {
564 let mut new_lines = self.lines();
565 let mut update_count = 0;
566
567 for line in &mut new_lines {
568 if predicate(line) {
569 *line = updater(line);
570 update_count += 1;
571 }
572 }
573
574 if update_count > 0 {
575 self.replace_with(new_lines.iter().map(|s| s.as_str()).collect());
576 }
577
578 update_count
579 }
580
581 pub fn split_into_bullets(&self) -> Vec<Change> {
613 let mut result = Vec::new();
614 let mut current_bullet_tokens = Vec::new();
615 let mut current_bullet_line_numbers = Vec::new();
616
617 for (i, token) in self.detail_tokens.iter().enumerate() {
618 let text = token.text();
619 let line_number = self.line_numbers.get(i).copied().unwrap_or(0);
620
621 if text.starts_with("* ") {
623 if !current_bullet_tokens.is_empty() {
625 result.push(Change::new(
626 self.entry.clone(),
627 self.author.clone(),
628 current_bullet_line_numbers.clone(),
629 current_bullet_tokens.clone(),
630 ));
631 current_bullet_tokens.clear();
632 current_bullet_line_numbers.clear();
633 }
634
635 current_bullet_tokens.push(token.clone());
637 current_bullet_line_numbers.push(line_number);
638 } else {
639 current_bullet_tokens.push(token.clone());
641 current_bullet_line_numbers.push(line_number);
642 }
643 }
644
645 if !current_bullet_tokens.is_empty() {
647 result.push(Change::new(
648 self.entry.clone(),
649 self.author.clone(),
650 current_bullet_line_numbers,
651 current_bullet_tokens,
652 ));
653 }
654
655 result
656 }
657}
658
659#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
662#[allow(non_camel_case_types)]
663#[repr(u16)]
664#[allow(missing_docs)]
665pub enum SyntaxKind {
666 IDENTIFIER = 0,
667 INDENT,
668 TEXT,
669 WHITESPACE,
670 VERSION, SEMICOLON, EQUALS, DETAIL, NEWLINE, ERROR, COMMENT, ROOT, ENTRY, ENTRY_HEADER,
682 ENTRY_FOOTER,
683 METADATA,
684 METADATA_ENTRY,
685 METADATA_KEY,
686 METADATA_VALUE,
687 ENTRY_BODY,
688 DISTRIBUTIONS,
689 EMPTY_LINE,
690
691 TIMESTAMP,
692 MAINTAINER,
693 EMAIL,
694}
695
696impl From<SyntaxKind> for rowan::SyntaxKind {
698 fn from(kind: SyntaxKind) -> Self {
699 Self(kind as u16)
700 }
701}
702
703pub fn parseaddr(s: &str) -> (Option<&str>, &str) {
711 if let Some((_, name, email)) = regex_captures!(r"^(.*)\s+<(.*)>$", s) {
712 if name.is_empty() {
713 (None, email)
714 } else {
715 (Some(name), email)
716 }
717 } else {
718 (None, s)
719 }
720}
721
722pub fn get_maintainer_from_env(
724 get_env: impl Fn(&str) -> Option<String>,
725) -> Option<(String, String)> {
726 use std::io::BufRead;
727
728 let mut debemail = get_env("DEBEMAIL");
729 let mut debfullname = get_env("DEBFULLNAME");
730
731 if let Some(email) = debemail.as_ref() {
733 let (parsed_name, parsed_email) = parseaddr(email);
734 if let Some(parsed_name) = parsed_name {
735 if debfullname.is_none() {
736 debfullname = Some(parsed_name.to_string());
737 }
738 }
739 debemail = Some(parsed_email.to_string());
740 }
741 if debfullname.is_none() || debemail.is_none() {
742 if let Some(email) = get_env("EMAIL") {
743 let (parsed_name, parsed_email) = parseaddr(email.as_str());
744 if let Some(parsed_name) = parsed_name {
745 if debfullname.is_none() {
746 debfullname = Some(parsed_name.to_string());
747 }
748 }
749 debemail = Some(parsed_email.to_string());
750 }
751 }
752
753 let maintainer = if let Some(m) = debfullname {
755 Some(m.trim().to_string())
756 } else if let Some(m) = get_env("NAME") {
757 Some(m.trim().to_string())
758 } else {
759 Some(whoami::realname())
760 };
761
762 let email_address = if let Some(email) = debemail {
764 Some(email)
765 } else if let Some(email) = get_env("EMAIL") {
766 Some(email)
767 } else {
768 let mut addr: Option<String> = None;
770
771 if let Ok(mailname_file) = std::fs::File::open("/etc/mailname") {
772 let mut reader = std::io::BufReader::new(mailname_file);
773 if let Ok(line) = reader.fill_buf() {
774 if !line.is_empty() {
775 addr = Some(String::from_utf8_lossy(line).trim().to_string());
776 }
777 }
778 }
779
780 if addr.is_none() {
781 match whoami::fallible::hostname() {
782 Ok(hostname) => {
783 addr = Some(hostname);
784 }
785 Err(e) => {
786 log::debug!("Failed to get hostname: {}", e);
787 addr = None;
788 }
789 }
790 }
791
792 addr.map(|hostname| format!("{}@{}", whoami::username(), hostname))
793 };
794
795 if let (Some(maintainer), Some(email_address)) = (maintainer, email_address) {
796 Some((maintainer, email_address))
797 } else {
798 None
799 }
800}
801
802pub fn get_maintainer() -> Option<(String, String)> {
817 get_maintainer_from_env(|s| std::env::var(s).ok())
818}
819
820#[cfg(test)]
821mod get_maintainer_from_env_tests {
822 use super::*;
823
824 #[test]
825 fn test_normal() {
826 get_maintainer();
827 }
828
829 #[test]
830 fn test_deb_vars() {
831 let mut d = std::collections::HashMap::new();
832 d.insert("DEBFULLNAME".to_string(), "Jelmer".to_string());
833 d.insert("DEBEMAIL".to_string(), "jelmer@example.com".to_string());
834 let t = get_maintainer_from_env(|s| d.get(s).cloned());
835 assert_eq!(
836 Some(("Jelmer".to_string(), "jelmer@example.com".to_string())),
837 t
838 );
839 }
840
841 #[test]
842 fn test_email_var() {
843 let mut d = std::collections::HashMap::new();
844 d.insert("NAME".to_string(), "Jelmer".to_string());
845 d.insert("EMAIL".to_string(), "foo@example.com".to_string());
846 let t = get_maintainer_from_env(|s| d.get(s).cloned());
847 assert_eq!(
848 Some(("Jelmer".to_string(), "foo@example.com".to_string())),
849 t
850 );
851 }
852}
853
854#[derive(Debug, Clone, PartialEq, Eq, Hash)]
856pub struct Identity {
857 pub name: String,
859
860 pub email: String,
862}
863
864impl Identity {
865 pub fn new(name: String, email: String) -> Self {
867 Self { name, email }
868 }
869
870 pub fn from_env() -> Option<Self> {
872 get_maintainer().map(|(name, email)| Self { name, email })
873 }
874}
875
876impl From<(String, String)> for Identity {
877 fn from((name, email): (String, String)) -> Self {
878 Self { name, email }
879 }
880}
881
882impl std::fmt::Display for Identity {
883 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
884 write!(f, "{} <{}>", self.name, self.email)
885 }
886}
887
888pub const UNRELEASED: &str = "UNRELEASED";
890const UNRELEASED_PREFIX: &str = "UNRELEASED-";
892
893pub fn distribution_is_unreleased(distribution: &str) -> bool {
895 distribution == UNRELEASED || distribution.starts_with(UNRELEASED_PREFIX)
896}
897
898pub fn distributions_is_unreleased(distributions: &[&str]) -> bool {
900 distributions.iter().any(|x| distribution_is_unreleased(x))
901}
902
903#[test]
904fn test_distributions_is_unreleased() {
905 assert!(distributions_is_unreleased(&["UNRELEASED"]));
906 assert!(distributions_is_unreleased(&[
907 "UNRELEASED-1",
908 "UNRELEASED-2"
909 ]));
910 assert!(distributions_is_unreleased(&["UNRELEASED", "UNRELEASED-2"]));
911 assert!(!distributions_is_unreleased(&["stable"]));
912}
913
914pub fn is_unreleased_inaugural(cl: &ChangeLog) -> bool {
916 let mut entries = cl.iter();
917 if let Some(entry) = entries.next() {
918 if entry.is_unreleased() == Some(false) {
919 return false;
920 }
921 let changes = entry.change_lines().collect::<Vec<_>>();
922 if changes.len() > 1 || !changes[0].starts_with("* Initial release") {
923 return false;
924 }
925 entries.next().is_none()
926 } else {
927 false
928 }
929}
930
931#[cfg(test)]
932mod is_unreleased_inaugural_tests {
933 use super::*;
934
935 #[test]
936 fn test_empty() {
937 assert!(!is_unreleased_inaugural(&ChangeLog::new()));
938 }
939
940 #[test]
941 fn test_unreleased_inaugural() {
942 let mut cl = ChangeLog::new();
943 cl.new_entry()
944 .maintainer(("Jelmer Vernooij".into(), "jelmer@debian.org".into()))
945 .distribution(UNRELEASED.to_string())
946 .version("1.0.0".parse().unwrap())
947 .change_line("* Initial release".to_string())
948 .finish();
949 assert!(is_unreleased_inaugural(&cl));
950 }
951
952 #[test]
953 fn test_not_unreleased_inaugural() {
954 let mut cl = ChangeLog::new();
955 cl.new_entry()
956 .maintainer(("Jelmer Vernooij".into(), "jelmer@debian.org".into()))
957 .distributions(vec!["unstable".to_string()])
958 .version("1.0.0".parse().unwrap())
959 .change_line("* Initial release".to_string())
960 .finish();
961 assert_eq!(cl.iter().next().unwrap().is_unreleased(), Some(false));
962
963 assert!(!is_unreleased_inaugural(&cl));
965
966 cl.new_entry()
967 .maintainer(("Jelmer Vernooij".into(), "jelmer@debian.org".into()))
968 .distribution(UNRELEASED.to_string())
969 .version("1.0.1".parse().unwrap())
970 .change_line("* Some change".to_string())
971 .finish();
972 assert!(!is_unreleased_inaugural(&cl));
974 }
975}
976
977const DEFAULT_DISTRIBUTION: &[&str] = &[UNRELEASED];
978
979pub fn release(
995 cl: &mut ChangeLog,
996 distribution: Option<Vec<String>>,
997 timestamp: Option<impl IntoTimestamp>,
998 maintainer: Option<(String, String)>,
999) -> bool {
1000 let mut entries = cl.iter();
1001 let mut first_entry = entries.next().unwrap();
1002 let second_entry = entries.next();
1003 let distribution = distribution.unwrap_or_else(|| {
1004 second_entry
1006 .and_then(|e| e.distributions())
1007 .unwrap_or_else(|| {
1008 DEFAULT_DISTRIBUTION
1009 .iter()
1010 .map(|s| s.to_string())
1011 .collect::<Vec<_>>()
1012 })
1013 });
1014 if first_entry.is_unreleased() == Some(false) {
1015 take_uploadership(&mut first_entry, maintainer);
1016 first_entry.set_distributions(distribution);
1017 let timestamp_str = if let Some(ts) = timestamp {
1018 ts.into_timestamp()
1019 } else {
1020 #[cfg(feature = "chrono")]
1021 {
1022 chrono::offset::Utc::now().into_timestamp()
1023 }
1024 #[cfg(not(feature = "chrono"))]
1025 {
1026 panic!("timestamp is required when chrono feature is disabled");
1027 }
1028 };
1029 first_entry.set_timestamp(timestamp_str);
1030 true
1031 } else {
1032 false
1033 }
1034}
1035
1036pub fn take_uploadership(entry: &mut Entry, maintainer: Option<(String, String)>) {
1042 let (maintainer_name, maintainer_email) = if let Some(m) = maintainer {
1043 m
1044 } else {
1045 get_maintainer().unwrap()
1046 };
1047 if let (Some(current_maintainer), Some(current_email)) = (entry.maintainer(), entry.email()) {
1048 if current_maintainer != maintainer_name || current_email != maintainer_email {
1049 if let Some(first_line) = entry.change_lines().next() {
1050 if first_line.starts_with("[ ") {
1051 entry.prepend_change_line(
1052 crate::changes::format_section_title(current_maintainer.as_str()).as_str(),
1053 );
1054 }
1055 }
1056 }
1057 }
1058 entry.set_maintainer((maintainer_name, maintainer_email));
1059}
1060
1061pub fn gbp_dch(path: &std::path::Path) -> std::result::Result<(), std::io::Error> {
1063 let output = std::process::Command::new("gbp")
1065 .arg("dch")
1066 .arg("--ignore-branch")
1067 .current_dir(path)
1068 .output()?;
1069
1070 if !output.status.success() {
1071 return Err(std::io::Error::other(format!(
1072 "gbp dch failed: {}",
1073 String::from_utf8_lossy(&output.stderr)
1074 )));
1075 }
1076
1077 Ok(())
1078}
1079
1080pub fn iter_entries_by_author(
1091 changelog: &ChangeLog,
1092) -> impl Iterator<Item = (String, String, Vec<Entry>)> + '_ {
1093 use std::collections::BTreeMap;
1094
1095 let mut grouped: BTreeMap<(String, String), Vec<Entry>> = BTreeMap::new();
1096
1097 for entry in changelog.iter() {
1098 let maintainer_name = entry.maintainer().unwrap_or_else(|| "Unknown".to_string());
1099 let maintainer_email = entry
1100 .email()
1101 .unwrap_or_else(|| "unknown@unknown".to_string());
1102 let key = (maintainer_name, maintainer_email);
1103
1104 grouped.entry(key).or_default().push(entry);
1105 }
1106
1107 grouped
1108 .into_iter()
1109 .map(|((name, email), entries)| (name, email, entries))
1110}
1111
1112pub fn iter_changes_by_author(changelog: &ChangeLog) -> Vec<Change> {
1123 let mut result = Vec::new();
1124
1125 for entry in changelog.iter() {
1126 let changes: Vec<String> = entry.change_lines().map(|s| s.to_string()).collect();
1127
1128 let all_detail_tokens: Vec<SyntaxToken> = entry
1130 .syntax()
1131 .children()
1132 .flat_map(|n| {
1133 n.children_with_tokens()
1134 .filter_map(|it| it.as_token().cloned())
1135 .filter(|token| token.kind() == SyntaxKind::DETAIL)
1136 })
1137 .collect();
1138
1139 let mut token_index = 0;
1141
1142 for (author, linenos, lines) in
1143 crate::changes::changes_by_author(changes.iter().map(|s| s.as_str()))
1144 {
1145 let author_name = author.map(|s| s.to_string());
1146
1147 let detail_tokens: Vec<SyntaxToken> = lines
1150 .iter()
1151 .filter_map(|line_text| {
1152 while token_index < all_detail_tokens.len() {
1154 let token = &all_detail_tokens[token_index];
1155 token_index += 1;
1156 if token.text() == *line_text {
1157 return Some(token.clone());
1158 }
1159 }
1160 None
1161 })
1162 .collect();
1163
1164 let change = Change::new(entry.clone(), author_name, linenos, detail_tokens);
1165 result.push(change);
1166 }
1167 }
1168
1169 result
1170}
1171
1172#[cfg(test)]
1173mod tests {
1174 use super::*;
1175
1176 #[test]
1177 fn test_parseaddr() {
1178 assert_eq!(
1179 (Some("Jelmer"), "jelmer@jelmer.uk"),
1180 parseaddr("Jelmer <jelmer@jelmer.uk>")
1181 );
1182 assert_eq!((None, "jelmer@jelmer.uk"), parseaddr("jelmer@jelmer.uk"));
1183 }
1184
1185 #[test]
1186 fn test_parseaddr_empty() {
1187 assert_eq!((None, ""), parseaddr(""));
1188 }
1189
1190 #[test]
1191 #[cfg(feature = "chrono")]
1192 fn test_release_already_released() {
1193 use crate::parse::ChangeLog;
1194
1195 let mut changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1196
1197 * New upstream release.
1198
1199 -- Jelmer Vernooij <jelmer@debian.org> Mon, 04 Sep 2023 18:13:45 -0500
1200"#
1201 .parse()
1202 .unwrap();
1203
1204 let result = release(
1205 &mut changelog,
1206 Some(vec!["unstable".to_string()]),
1207 None::<String>,
1208 None,
1209 );
1210
1211 assert!(result);
1213 }
1214
1215 #[test]
1216 #[cfg(feature = "chrono")]
1217 fn test_release_unreleased() {
1218 use crate::parse::ChangeLog;
1219
1220 let mut changelog: ChangeLog = r#"breezy (3.3.4-1) UNRELEASED; urgency=low
1221
1222 * New upstream release.
1223
1224 -- Jelmer Vernooij <jelmer@debian.org> Mon, 04 Sep 2023 18:13:45 -0500
1225"#
1226 .parse()
1227 .unwrap();
1228
1229 let result = release(
1230 &mut changelog,
1231 Some(vec!["unstable".to_string()]),
1232 None::<String>,
1233 Some(("Test User".to_string(), "test@example.com".to_string())),
1234 );
1235
1236 assert!(!result);
1238 }
1239
1240 #[test]
1241 fn test_take_uploadership_same_maintainer() {
1242 use crate::parse::ChangeLog;
1243
1244 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1245
1246 * New upstream release.
1247
1248 -- Test User <test@example.com> Mon, 04 Sep 2023 18:13:45 -0500
1249"#
1250 .parse()
1251 .unwrap();
1252
1253 let mut entries: Vec<Entry> = changelog.into_iter().collect();
1254 take_uploadership(
1255 &mut entries[0],
1256 Some(("Test User".to_string(), "test@example.com".to_string())),
1257 );
1258
1259 assert!(!entries[0].to_string().contains("[ Test User ]"));
1261 }
1262
1263 #[test]
1264 fn test_take_uploadership_different_maintainer() {
1265 use crate::parse::ChangeLog;
1266
1267 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1268
1269 * New upstream release.
1270
1271 -- Original User <original@example.com> Mon, 04 Sep 2023 18:13:45 -0500
1272"#
1273 .parse()
1274 .unwrap();
1275
1276 let mut entries: Vec<Entry> = changelog.into_iter().collect();
1277
1278 take_uploadership(
1279 &mut entries[0],
1280 Some(("New User".to_string(), "new@example.com".to_string())),
1281 );
1282
1283 assert!(entries[0]
1285 .to_string()
1286 .contains("New User <new@example.com>"));
1287 assert_eq!(entries[0].email(), Some("new@example.com".to_string()));
1288 }
1289
1290 #[test]
1291 fn test_identity_display() {
1292 let identity = Identity {
1293 name: "Test User".to_string(),
1294 email: "test@example.com".to_string(),
1295 };
1296 assert_eq!(identity.to_string(), "Test User <test@example.com>");
1297
1298 let identity_empty_name = Identity {
1299 name: "".to_string(),
1300 email: "test@example.com".to_string(),
1301 };
1302 assert_eq!(identity_empty_name.to_string(), " <test@example.com>");
1303 }
1304
1305 #[test]
1306 fn test_gbp_dch_failure() {
1307 let result = gbp_dch(std::path::Path::new("/nonexistent/path"));
1309 assert!(result.is_err());
1310 }
1311
1312 #[test]
1313 fn test_iter_entries_by_author() {
1314 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1315
1316 * New upstream release.
1317
1318 -- Jelmer Vernooij <jelmer@debian.org> Mon, 04 Sep 2023 18:13:45 -0500
1319
1320breezy (3.3.3-1) unstable; urgency=low
1321
1322 * Bug fix release.
1323
1324 -- Jelmer Vernooij <jelmer@debian.org> Sun, 03 Sep 2023 17:12:30 -0500
1325
1326breezy (3.3.2-1) unstable; urgency=low
1327
1328 * Another release.
1329
1330 -- Jane Doe <jane@example.com> Sat, 02 Sep 2023 16:11:15 -0500
1331"#
1332 .parse()
1333 .unwrap();
1334
1335 let authors: Vec<(String, String, Vec<Entry>)> =
1336 iter_entries_by_author(&changelog).collect();
1337
1338 assert_eq!(authors.len(), 2);
1339 assert_eq!(authors[0].0, "Jane Doe");
1340 assert_eq!(authors[0].1, "jane@example.com");
1341 assert_eq!(authors[0].2.len(), 1);
1342 assert_eq!(authors[1].0, "Jelmer Vernooij");
1343 assert_eq!(authors[1].1, "jelmer@debian.org");
1344 assert_eq!(authors[1].2.len(), 2);
1345 }
1346
1347 #[test]
1348 fn test_iter_changes_by_author() {
1349 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1350
1351 [ Author 1 ]
1352 * Change by Author 1
1353
1354 [ Author 2 ]
1355 * Change by Author 2
1356
1357 * Unattributed change
1358
1359 -- Jelmer Vernooij <jelmer@debian.org> Mon, 04 Sep 2023 18:13:45 -0500
1360"#
1361 .parse()
1362 .unwrap();
1363
1364 let changes = iter_changes_by_author(&changelog);
1365
1366 assert_eq!(changes.len(), 3);
1367
1368 assert_eq!(changes[0].author(), Some("Author 1"));
1370 assert_eq!(changes[0].package(), Some("breezy".to_string()));
1371 assert_eq!(changes[0].lines(), vec!["* Change by Author 1"]);
1372
1373 assert_eq!(changes[1].author(), Some("Author 2"));
1375 assert_eq!(changes[1].package(), Some("breezy".to_string()));
1376 assert_eq!(changes[1].lines(), vec!["* Change by Author 2"]);
1377
1378 assert_eq!(changes[2].author(), None);
1380 assert_eq!(changes[2].package(), Some("breezy".to_string()));
1381 assert_eq!(changes[2].lines(), vec!["* Unattributed change"]);
1382 }
1383
1384 #[test]
1385 fn test_change_remove() {
1386 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1387
1388 [ Author 1 ]
1389 * Change by Author 1
1390
1391 [ Author 2 ]
1392 * Change by Author 2
1393
1394 * Unattributed change
1395
1396 -- Jelmer Vernooij <jelmer@debian.org> Mon, 04 Sep 2023 18:13:45 -0500
1397"#
1398 .parse()
1399 .unwrap();
1400
1401 let changes = iter_changes_by_author(&changelog);
1402 assert_eq!(changes.len(), 3);
1403
1404 changes[1].clone().remove();
1406
1407 let remaining_changes = iter_changes_by_author(&changelog);
1409
1410 assert_eq!(remaining_changes.len(), 2);
1414
1415 assert_eq!(remaining_changes[0].author(), Some("Author 1"));
1417 assert_eq!(remaining_changes[0].lines(), vec!["* Change by Author 1"]);
1418
1419 assert_eq!(remaining_changes[1].author(), Some("Author 2"));
1421 assert_eq!(remaining_changes[1].lines(), vec!["* Unattributed change"]);
1422 }
1423
1424 #[test]
1425 fn test_change_replace_with() {
1426 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1427
1428 [ Author 1 ]
1429 * Change by Author 1
1430
1431 [ Author 2 ]
1432 * Change by Author 2
1433
1434 -- Jelmer Vernooij <jelmer@debian.org> Mon, 04 Sep 2023 18:13:45 -0500
1435"#
1436 .parse()
1437 .unwrap();
1438
1439 let changes = iter_changes_by_author(&changelog);
1440 assert_eq!(changes.len(), 2);
1441
1442 changes[1].replace_with(vec!["* Updated change by Author 2", "* Another line"]);
1444
1445 let updated_changes = iter_changes_by_author(&changelog);
1447 assert_eq!(updated_changes.len(), 2);
1448
1449 assert_eq!(updated_changes[0].author(), Some("Author 1"));
1451 assert_eq!(updated_changes[0].lines(), vec!["* Change by Author 1"]);
1452
1453 assert_eq!(updated_changes[1].author(), Some("Author 2"));
1455 assert_eq!(
1456 updated_changes[1].lines(),
1457 vec!["* Updated change by Author 2", "* Another line"]
1458 );
1459 }
1460
1461 #[test]
1462 fn test_change_replace_with_single_line() {
1463 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1464
1465 * Old change
1466
1467 -- Jelmer Vernooij <jelmer@debian.org> Mon, 04 Sep 2023 18:13:45 -0500
1468"#
1469 .parse()
1470 .unwrap();
1471
1472 let changes = iter_changes_by_author(&changelog);
1473 assert_eq!(changes.len(), 1);
1474
1475 changes[0].replace_with(vec!["* New change"]);
1477
1478 let updated_changes = iter_changes_by_author(&changelog);
1480 assert_eq!(updated_changes.len(), 1);
1481 assert_eq!(updated_changes[0].lines(), vec!["* New change"]);
1482 }
1483
1484 #[test]
1485 fn test_change_accessors() {
1486 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1487
1488 [ Alice ]
1489 * Change by Alice
1490
1491 -- Bob <bob@example.com> Mon, 04 Sep 2023 18:13:45 -0500
1492"#
1493 .parse()
1494 .unwrap();
1495
1496 let changes = iter_changes_by_author(&changelog);
1497 assert_eq!(changes.len(), 1);
1498
1499 let change = &changes[0];
1500
1501 assert_eq!(change.author(), Some("Alice"));
1503 assert_eq!(change.package(), Some("breezy".to_string()));
1504 assert_eq!(
1505 change.version().map(|v| v.to_string()),
1506 Some("3.3.4-1".to_string())
1507 );
1508 assert_eq!(change.is_attributed(), true);
1509 assert_eq!(change.lines(), vec!["* Change by Alice"]);
1510
1511 assert_eq!(change.entry().package(), Some("breezy".to_string()));
1513 }
1514
1515 #[test]
1516 fn test_change_unattributed_accessors() {
1517 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1518
1519 * Unattributed change
1520
1521 -- Bob <bob@example.com> Mon, 04 Sep 2023 18:13:45 -0500
1522"#
1523 .parse()
1524 .unwrap();
1525
1526 let changes = iter_changes_by_author(&changelog);
1527 assert_eq!(changes.len(), 1);
1528
1529 let change = &changes[0];
1530 assert_eq!(change.author(), None);
1531 assert_eq!(change.is_attributed(), false);
1532 }
1533
1534 #[test]
1535 fn test_replace_single_line_with_multiple() {
1536 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1537
1538 * Single line change
1539
1540 -- Bob <bob@example.com> Mon, 04 Sep 2023 18:13:45 -0500
1541"#
1542 .parse()
1543 .unwrap();
1544
1545 let changes = iter_changes_by_author(&changelog);
1546 changes[0].replace_with(vec!["* First line", "* Second line", "* Third line"]);
1547
1548 let updated = iter_changes_by_author(&changelog);
1549 assert_eq!(
1550 updated[0].lines(),
1551 vec!["* First line", "* Second line", "* Third line"]
1552 );
1553 }
1554
1555 #[test]
1556 fn test_replace_multiple_lines_with_single() {
1557 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1558
1559 * First line
1560 * Second line
1561 * Third line
1562
1563 -- Bob <bob@example.com> Mon, 04 Sep 2023 18:13:45 -0500
1564"#
1565 .parse()
1566 .unwrap();
1567
1568 let changes = iter_changes_by_author(&changelog);
1569 assert_eq!(changes[0].lines().len(), 3);
1570
1571 changes[0].replace_with(vec!["* Single replacement line"]);
1572
1573 let updated = iter_changes_by_author(&changelog);
1574 assert_eq!(updated[0].lines(), vec!["* Single replacement line"]);
1575 }
1576
1577 #[test]
1578 fn test_split_into_bullets_single_line() {
1579 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1580
1581 * First change
1582 * Second change
1583 * Third change
1584
1585 -- Bob <bob@example.com> Mon, 04 Sep 2023 18:13:45 -0500
1586"#
1587 .parse()
1588 .unwrap();
1589
1590 let changes = iter_changes_by_author(&changelog);
1591 assert_eq!(changes.len(), 1);
1592
1593 let bullets = changes[0].split_into_bullets();
1595
1596 assert_eq!(bullets.len(), 3);
1597 assert_eq!(bullets[0].lines(), vec!["* First change"]);
1598 assert_eq!(bullets[1].lines(), vec!["* Second change"]);
1599 assert_eq!(bullets[2].lines(), vec!["* Third change"]);
1600
1601 for bullet in &bullets {
1603 assert_eq!(bullet.package(), Some("breezy".to_string()));
1604 assert_eq!(
1605 bullet.version().map(|v| v.to_string()),
1606 Some("3.3.4-1".to_string())
1607 );
1608 }
1609 }
1610
1611 #[test]
1612 fn test_split_into_bullets_with_continuations() {
1613 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1614
1615 * First change
1616 with a continuation line
1617 * Second change
1618 with multiple
1619 continuation lines
1620 * Third change
1621
1622 -- Bob <bob@example.com> Mon, 04 Sep 2023 18:13:45 -0500
1623"#
1624 .parse()
1625 .unwrap();
1626
1627 let changes = iter_changes_by_author(&changelog);
1628 assert_eq!(changes.len(), 1);
1629
1630 let bullets = changes[0].split_into_bullets();
1631
1632 assert_eq!(bullets.len(), 3);
1633 assert_eq!(
1634 bullets[0].lines(),
1635 vec!["* First change", " with a continuation line"]
1636 );
1637 assert_eq!(
1638 bullets[1].lines(),
1639 vec!["* Second change", " with multiple", " continuation lines"]
1640 );
1641 assert_eq!(bullets[2].lines(), vec!["* Third change"]);
1642 }
1643
1644 #[test]
1645 fn test_split_into_bullets_mixed() {
1646 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1647
1648 * Single line bullet
1649 * Multi-line bullet
1650 with continuation
1651 * Another single line
1652
1653 -- Bob <bob@example.com> Mon, 04 Sep 2023 18:13:45 -0500
1654"#
1655 .parse()
1656 .unwrap();
1657
1658 let changes = iter_changes_by_author(&changelog);
1659 let bullets = changes[0].split_into_bullets();
1660
1661 assert_eq!(bullets.len(), 3);
1662 assert_eq!(bullets[0].lines(), vec!["* Single line bullet"]);
1663 assert_eq!(
1664 bullets[1].lines(),
1665 vec!["* Multi-line bullet", " with continuation"]
1666 );
1667 assert_eq!(bullets[2].lines(), vec!["* Another single line"]);
1668 }
1669
1670 #[test]
1671 fn test_split_into_bullets_with_author() {
1672 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1673
1674 [ Alice ]
1675 * Change by Alice
1676 * Another change by Alice
1677
1678 [ Bob ]
1679 * Change by Bob
1680
1681 -- Maintainer <maint@example.com> Mon, 04 Sep 2023 18:13:45 -0500
1682"#
1683 .parse()
1684 .unwrap();
1685
1686 let changes = iter_changes_by_author(&changelog);
1687 assert_eq!(changes.len(), 2);
1688
1689 let alice_bullets = changes[0].split_into_bullets();
1691 assert_eq!(alice_bullets.len(), 2);
1692 assert_eq!(alice_bullets[0].lines(), vec!["* Change by Alice"]);
1693 assert_eq!(alice_bullets[1].lines(), vec!["* Another change by Alice"]);
1694
1695 for bullet in &alice_bullets {
1697 assert_eq!(bullet.author(), Some("Alice"));
1698 }
1699
1700 let bob_bullets = changes[1].split_into_bullets();
1702 assert_eq!(bob_bullets.len(), 1);
1703 assert_eq!(bob_bullets[0].lines(), vec!["* Change by Bob"]);
1704 assert_eq!(bob_bullets[0].author(), Some("Bob"));
1705 }
1706
1707 #[test]
1708 fn test_split_into_bullets_single_bullet() {
1709 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1710
1711 * Single bullet point
1712
1713 -- Bob <bob@example.com> Mon, 04 Sep 2023 18:13:45 -0500
1714"#
1715 .parse()
1716 .unwrap();
1717
1718 let changes = iter_changes_by_author(&changelog);
1719 let bullets = changes[0].split_into_bullets();
1720
1721 assert_eq!(bullets.len(), 1);
1722 assert_eq!(bullets[0].lines(), vec!["* Single bullet point"]);
1723 }
1724
1725 #[test]
1726 fn test_split_into_bullets_and_remove() {
1727 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1728
1729 * First change
1730 * Duplicate change
1731 * Duplicate change
1732 * Last change
1733
1734 -- Bob <bob@example.com> Mon, 04 Sep 2023 18:13:45 -0500
1735"#
1736 .parse()
1737 .unwrap();
1738
1739 let changes = iter_changes_by_author(&changelog);
1740 let bullets = changes[0].split_into_bullets();
1741
1742 assert_eq!(bullets.len(), 4);
1743
1744 bullets[2].clone().remove();
1746
1747 let updated_changes = iter_changes_by_author(&changelog);
1749 let updated_bullets = updated_changes[0].split_into_bullets();
1750
1751 assert_eq!(updated_bullets.len(), 3);
1752 assert_eq!(updated_bullets[0].lines(), vec!["* First change"]);
1753 assert_eq!(updated_bullets[1].lines(), vec!["* Duplicate change"]);
1754 assert_eq!(updated_bullets[2].lines(), vec!["* Last change"]);
1755 }
1756
1757 #[test]
1758 fn test_split_into_bullets_preserves_line_numbers() {
1759 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1760
1761 * First change
1762 * Second change
1763 * Third change
1764
1765 -- Bob <bob@example.com> Mon, 04 Sep 2023 18:13:45 -0500
1766"#
1767 .parse()
1768 .unwrap();
1769
1770 let changes = iter_changes_by_author(&changelog);
1771 let bullets = changes[0].split_into_bullets();
1772
1773 assert_eq!(bullets.len(), 3);
1775 assert_eq!(bullets[0].line_numbers().len(), 1);
1776 assert_eq!(bullets[1].line_numbers().len(), 1);
1777 assert_eq!(bullets[2].line_numbers().len(), 1);
1778
1779 assert!(bullets[0].line_numbers()[0] < bullets[1].line_numbers()[0]);
1781 assert!(bullets[1].line_numbers()[0] < bullets[2].line_numbers()[0]);
1782 }
1783
1784 #[test]
1785 fn test_split_and_remove_from_multi_author_entry() {
1786 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1787
1788 [ Alice ]
1789 * Change 1 by Alice
1790 * Change 2 by Alice
1791 * Change 3 by Alice
1792
1793 [ Bob ]
1794 * Change 1 by Bob
1795 * Change 2 by Bob
1796
1797 [ Charlie ]
1798 * Change 1 by Charlie
1799
1800 * Unattributed change
1801
1802 -- Maintainer <maint@example.com> Mon, 04 Sep 2023 18:13:45 -0500
1803"#
1804 .parse()
1805 .unwrap();
1806
1807 let changes = iter_changes_by_author(&changelog);
1808 assert_eq!(changes.len(), 4); let alice_bullets = changes[0].split_into_bullets();
1812 assert_eq!(alice_bullets.len(), 3);
1813 alice_bullets[1].clone().remove(); let updated_changes = iter_changes_by_author(&changelog);
1817 assert_eq!(updated_changes.len(), 4);
1818
1819 let updated_alice_bullets = updated_changes[0].split_into_bullets();
1821 assert_eq!(updated_alice_bullets.len(), 2);
1822 assert_eq!(
1823 updated_alice_bullets[0].lines(),
1824 vec!["* Change 1 by Alice"]
1825 );
1826 assert_eq!(
1827 updated_alice_bullets[1].lines(),
1828 vec!["* Change 3 by Alice"]
1829 );
1830 assert_eq!(updated_alice_bullets[0].author(), Some("Alice"));
1831
1832 let bob_bullets = updated_changes[1].split_into_bullets();
1834 assert_eq!(bob_bullets.len(), 2);
1835 assert_eq!(bob_bullets[0].lines(), vec!["* Change 1 by Bob"]);
1836 assert_eq!(bob_bullets[1].lines(), vec!["* Change 2 by Bob"]);
1837
1838 let charlie_bullets = updated_changes[2].split_into_bullets();
1840 assert_eq!(charlie_bullets.len(), 1);
1841 assert_eq!(charlie_bullets[0].lines(), vec!["* Change 1 by Charlie"]);
1842
1843 let unattributed_bullets = updated_changes[3].split_into_bullets();
1845 assert_eq!(unattributed_bullets.len(), 1);
1846 assert_eq!(
1847 unattributed_bullets[0].lines(),
1848 vec!["* Unattributed change"]
1849 );
1850 }
1851
1852 #[test]
1853 fn test_remove_multiple_bullets_from_different_authors() {
1854 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1855
1856 [ Alice ]
1857 * Alice change 1
1858 * Alice change 2
1859 * Alice change 3
1860
1861 [ Bob ]
1862 * Bob change 1
1863 * Bob change 2
1864 * Bob change 3
1865
1866 -- Maintainer <maint@example.com> Mon, 04 Sep 2023 18:13:45 -0500
1867"#
1868 .parse()
1869 .unwrap();
1870
1871 let changes = iter_changes_by_author(&changelog);
1872 assert_eq!(changes.len(), 2);
1873
1874 let alice_bullets = changes[0].split_into_bullets();
1876 alice_bullets[0].clone().remove();
1877 alice_bullets[2].clone().remove();
1878
1879 let bob_bullets = changes[1].split_into_bullets();
1881 bob_bullets[1].clone().remove();
1882
1883 let updated_changes = iter_changes_by_author(&changelog);
1885
1886 let updated_alice = updated_changes[0].split_into_bullets();
1887 assert_eq!(updated_alice.len(), 1);
1888 assert_eq!(updated_alice[0].lines(), vec!["* Alice change 2"]);
1889
1890 let updated_bob = updated_changes[1].split_into_bullets();
1891 assert_eq!(updated_bob.len(), 2);
1892 assert_eq!(updated_bob[0].lines(), vec!["* Bob change 1"]);
1893 assert_eq!(updated_bob[1].lines(), vec!["* Bob change 3"]);
1894 }
1895
1896 #[test]
1897 fn test_remove_bullet_with_continuation_from_multi_author() {
1898 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1899
1900 [ Alice ]
1901 * Simple change by Alice
1902
1903 [ Bob ]
1904 * Multi-line change by Bob
1905 with a continuation line
1906 and another continuation
1907 * Simple change by Bob
1908
1909 [ Charlie ]
1910 * Change by Charlie
1911
1912 -- Maintainer <maint@example.com> Mon, 04 Sep 2023 18:13:45 -0500
1913"#
1914 .parse()
1915 .unwrap();
1916
1917 let changes = iter_changes_by_author(&changelog);
1918 assert_eq!(changes.len(), 3);
1919
1920 let bob_bullets = changes[1].split_into_bullets();
1922 assert_eq!(bob_bullets.len(), 2);
1923 assert_eq!(
1924 bob_bullets[0].lines(),
1925 vec![
1926 "* Multi-line change by Bob",
1927 " with a continuation line",
1928 " and another continuation"
1929 ]
1930 );
1931 bob_bullets[0].clone().remove();
1932
1933 let updated_changes = iter_changes_by_author(&changelog);
1935
1936 let alice_bullets = updated_changes[0].split_into_bullets();
1938 assert_eq!(alice_bullets.len(), 1);
1939 assert_eq!(alice_bullets[0].lines(), vec!["* Simple change by Alice"]);
1940
1941 let updated_bob = updated_changes[1].split_into_bullets();
1943 assert_eq!(updated_bob.len(), 1);
1944 assert_eq!(updated_bob[0].lines(), vec!["* Simple change by Bob"]);
1945
1946 let charlie_bullets = updated_changes[2].split_into_bullets();
1948 assert_eq!(charlie_bullets.len(), 1);
1949 assert_eq!(charlie_bullets[0].lines(), vec!["* Change by Charlie"]);
1950 }
1951
1952 #[test]
1953 fn test_remove_all_bullets_from_one_author_section() {
1954 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
1955
1956 [ Alice ]
1957 * Change 1 by Alice
1958 * Change 2 by Alice
1959
1960 [ Bob ]
1961 * Change 1 by Bob
1962
1963 -- Maintainer <maint@example.com> Mon, 04 Sep 2023 18:13:45 -0500
1964"#
1965 .parse()
1966 .unwrap();
1967
1968 let changes = iter_changes_by_author(&changelog);
1969 assert_eq!(changes.len(), 2);
1970
1971 let alice_bullets = changes[0].split_into_bullets();
1973 for bullet in alice_bullets {
1974 bullet.remove();
1975 }
1976
1977 let updated_changes = iter_changes_by_author(&changelog);
1979
1980 assert_eq!(updated_changes.len(), 1);
1983 assert_eq!(updated_changes[0].author(), Some("Bob"));
1984
1985 let bob_bullets = updated_changes[0].split_into_bullets();
1986 assert_eq!(bob_bullets.len(), 1);
1987 assert_eq!(bob_bullets[0].lines(), vec!["* Change 1 by Bob"]);
1988
1989 let changelog_text = changelog.to_string();
1991 assert!(
1992 !changelog_text.contains("[ Alice ]"),
1993 "Alice's empty section header should be removed"
1994 );
1995 }
1996
1997 #[test]
1998 fn test_remove_section_header_with_multiple_sections() {
1999 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
2000
2001 [ Alice ]
2002 * Alice's first section change
2003
2004 [ Bob ]
2005 * Bob's change
2006
2007 [ Alice ]
2008 * Alice's second section change 1
2009 * Alice's second section change 2
2010
2011 -- Maintainer <maint@example.com> Mon, 04 Sep 2023 18:13:45 -0500
2012"#
2013 .parse()
2014 .unwrap();
2015
2016 let changes = iter_changes_by_author(&changelog);
2017 assert_eq!(changes.len(), 3);
2018
2019 let alice_second = &changes[2];
2021 assert_eq!(alice_second.author(), Some("Alice"));
2022
2023 let alice_second_bullets = alice_second.split_into_bullets();
2024 assert_eq!(alice_second_bullets.len(), 2);
2025
2026 for bullet in alice_second_bullets {
2028 bullet.remove();
2029 }
2030
2031 let updated_changes = iter_changes_by_author(&changelog);
2033
2034 assert_eq!(updated_changes.len(), 2);
2036 assert_eq!(updated_changes[0].author(), Some("Alice"));
2037 assert_eq!(updated_changes[1].author(), Some("Bob"));
2038
2039 let alice_first = updated_changes[0].split_into_bullets();
2041 assert_eq!(alice_first.len(), 1);
2042 assert_eq!(
2043 alice_first[0].lines(),
2044 vec!["* Alice's first section change"]
2045 );
2046
2047 let changelog_text = changelog.to_string();
2049 let alice_header_count = changelog_text.matches("[ Alice ]").count();
2050 assert_eq!(
2051 alice_header_count, 1,
2052 "Should only have one Alice section header remaining"
2053 );
2054 }
2055
2056 #[test]
2057 fn test_remove_duplicate_from_specific_author() {
2058 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
2059
2060 [ Alice ]
2061 * New upstream release
2062 * Fix typo in documentation
2063 * New upstream release
2064
2065 [ Bob ]
2066 * New upstream release
2067 * Update dependencies
2068
2069 -- Maintainer <maint@example.com> Mon, 04 Sep 2023 18:13:45 -0500
2070"#
2071 .parse()
2072 .unwrap();
2073
2074 let changes = iter_changes_by_author(&changelog);
2075 assert_eq!(changes.len(), 2);
2076
2077 let alice_bullets = changes[0].split_into_bullets();
2079 assert_eq!(alice_bullets.len(), 3);
2080
2081 assert_eq!(alice_bullets[0].lines(), vec!["* New upstream release"]);
2083 assert_eq!(
2084 alice_bullets[1].lines(),
2085 vec!["* Fix typo in documentation"]
2086 );
2087 assert_eq!(alice_bullets[2].lines(), vec!["* New upstream release"]);
2088
2089 alice_bullets[2].clone().remove();
2091
2092 let updated_changes = iter_changes_by_author(&changelog);
2094
2095 let updated_alice = updated_changes[0].split_into_bullets();
2097 assert_eq!(updated_alice.len(), 2);
2098 assert_eq!(updated_alice[0].lines(), vec!["* New upstream release"]);
2099 assert_eq!(
2100 updated_alice[1].lines(),
2101 vec!["* Fix typo in documentation"]
2102 );
2103
2104 let bob_bullets = updated_changes[1].split_into_bullets();
2106 assert_eq!(bob_bullets.len(), 2);
2107 assert_eq!(bob_bullets[0].lines(), vec!["* New upstream release"]);
2108 assert_eq!(bob_bullets[1].lines(), vec!["* Update dependencies"]);
2109 }
2110
2111 #[test]
2112 fn test_remove_empty_section_headers_and_blank_lines() {
2113 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
2116
2117 [ Alice ]
2118 * Change 1 by Alice
2119 * Change 2 by Alice
2120
2121 [ Bob ]
2122 * Change 1 by Bob
2123
2124 -- Maintainer <maint@example.com> Mon, 04 Sep 2023 18:13:45 -0500
2125"#
2126 .parse()
2127 .unwrap();
2128
2129 let changes = iter_changes_by_author(&changelog);
2130 assert_eq!(changes.len(), 2);
2131
2132 let alice_bullets = changes[0].split_into_bullets();
2134 for bullet in alice_bullets {
2135 bullet.remove();
2136 }
2137
2138 let updated_changes = iter_changes_by_author(&changelog);
2140 assert_eq!(updated_changes.len(), 1);
2141 assert_eq!(updated_changes[0].author(), Some("Bob"));
2142
2143 let changelog_text = changelog.to_string();
2145 assert!(!changelog_text.contains("[ Alice ]"));
2146
2147 let lines: Vec<&str> = changelog_text.lines().collect();
2149 let sig_idx = lines.iter().position(|l| l.starts_with(" --")).unwrap();
2150 let mut blank_count = 0;
2151 for i in (0..sig_idx).rev() {
2152 if lines[i].trim().is_empty() {
2153 blank_count += 1;
2154 } else {
2155 break;
2156 }
2157 }
2158 assert_eq!(
2159 blank_count, 1,
2160 "Should have exactly 1 blank line before signature"
2161 );
2162 }
2163
2164 #[test]
2165 fn test_remove_first_entry_before_author_section() {
2166 let changelog: ChangeLog = r#"lintian-brush (0.1-2) UNRELEASED; urgency=medium
2169
2170 * Team upload.
2171
2172 [ Jelmer Vernooij ]
2173 * blah
2174
2175 -- Jelmer Vernooij <jelmer@debian.org> Fri, 23 Nov 2018 14:00:02 +0000
2176
2177lintian-brush (0.1-1) unstable; urgency=medium
2178
2179 * Initial release. (Closes: #XXXXXX)
2180
2181 -- Jelmer Vernooij <jelmer@debian.org> Sun, 28 Oct 2018 00:09:52 +0000
2182"#
2183 .parse()
2184 .unwrap();
2185
2186 let changes = iter_changes_by_author(&changelog);
2187
2188 let team_upload_change = changes
2190 .iter()
2191 .find(|c| c.lines().iter().any(|l| l.contains("Team upload")))
2192 .unwrap();
2193
2194 team_upload_change.clone().remove();
2195
2196 let expected = r#"lintian-brush (0.1-2) UNRELEASED; urgency=medium
2197
2198 [ Jelmer Vernooij ]
2199 * blah
2200
2201 -- Jelmer Vernooij <jelmer@debian.org> Fri, 23 Nov 2018 14:00:02 +0000
2202
2203lintian-brush (0.1-1) unstable; urgency=medium
2204
2205 * Initial release. (Closes: #XXXXXX)
2206
2207 -- Jelmer Vernooij <jelmer@debian.org> Sun, 28 Oct 2018 00:09:52 +0000
2208"#;
2209
2210 assert_eq!(changelog.to_string(), expected);
2211 }
2212
2213 fn test_remove_change(input: &str, change_filter: impl Fn(&Change) -> bool, expected: &str) {
2216 let changelog: ChangeLog = input.parse().unwrap();
2217 let changes = iter_changes_by_author(&changelog);
2218
2219 let mut all_bullets = Vec::new();
2221 for change in changes {
2222 all_bullets.extend(change.split_into_bullets());
2223 }
2224
2225 let change = all_bullets.iter().find(|c| change_filter(c)).unwrap();
2226 change.clone().remove();
2227 assert_eq!(changelog.to_string(), expected);
2228 }
2229
2230 #[test]
2231 fn test_remove_entry_followed_by_regular_bullet() {
2232 test_remove_change(
2234 r#"lintian-brush (0.1-2) UNRELEASED; urgency=medium
2235
2236 * First change.
2237
2238 * Second change.
2239
2240 -- Jelmer Vernooij <jelmer@debian.org> Fri, 23 Nov 2018 14:00:02 +0000
2241"#,
2242 |c| c.lines().iter().any(|l| l.contains("First change")),
2243 r#"lintian-brush (0.1-2) UNRELEASED; urgency=medium
2244
2245 * Second change.
2246
2247 -- Jelmer Vernooij <jelmer@debian.org> Fri, 23 Nov 2018 14:00:02 +0000
2248"#,
2249 );
2250 }
2251
2252 #[test]
2253 fn test_remove_entry_not_followed_by_empty_line() {
2254 test_remove_change(
2256 r#"lintian-brush (0.1-2) UNRELEASED; urgency=medium
2257
2258 * First change.
2259 * Second change.
2260
2261 -- Jelmer Vernooij <jelmer@debian.org> Fri, 23 Nov 2018 14:00:02 +0000
2262"#,
2263 |c| c.lines().iter().any(|l| l.contains("First change")),
2264 r#"lintian-brush (0.1-2) UNRELEASED; urgency=medium
2265
2266 * Second change.
2267
2268 -- Jelmer Vernooij <jelmer@debian.org> Fri, 23 Nov 2018 14:00:02 +0000
2269"#,
2270 );
2271 }
2272
2273 #[test]
2274 fn test_remove_only_entry() {
2275 test_remove_change(
2277 r#"lintian-brush (0.1-2) UNRELEASED; urgency=medium
2278
2279 * Only change.
2280
2281 -- Jelmer Vernooij <jelmer@debian.org> Fri, 23 Nov 2018 14:00:02 +0000
2282"#,
2283 |c| c.lines().iter().any(|l| l.contains("Only change")),
2284 r#"lintian-brush (0.1-2) UNRELEASED; urgency=medium
2285
2286 -- Jelmer Vernooij <jelmer@debian.org> Fri, 23 Nov 2018 14:00:02 +0000
2287"#,
2288 );
2289 }
2290
2291 #[test]
2292 fn test_remove_middle_entry_between_bullets() {
2293 test_remove_change(
2295 r#"lintian-brush (0.1-2) UNRELEASED; urgency=medium
2296
2297 * First change.
2298
2299 * Middle change.
2300
2301 * Last change.
2302
2303 -- Jelmer Vernooij <jelmer@debian.org> Fri, 23 Nov 2018 14:00:02 +0000
2304"#,
2305 |c| c.lines().iter().any(|l| l.contains("Middle change")),
2306 r#"lintian-brush (0.1-2) UNRELEASED; urgency=medium
2307
2308 * First change.
2309
2310 * Last change.
2311
2312 -- Jelmer Vernooij <jelmer@debian.org> Fri, 23 Nov 2018 14:00:02 +0000
2313"#,
2314 );
2315 }
2316
2317 #[test]
2318 fn test_remove_entry_before_multiple_section_headers() {
2319 test_remove_change(
2321 r#"lintian-brush (0.1-2) UNRELEASED; urgency=medium
2322
2323 * Team upload.
2324
2325 [ Author One ]
2326 * Change by author one.
2327
2328 [ Author Two ]
2329 * Change by author two.
2330
2331 -- Jelmer Vernooij <jelmer@debian.org> Fri, 23 Nov 2018 14:00:02 +0000
2332"#,
2333 |c| c.lines().iter().any(|l| l.contains("Team upload")),
2334 r#"lintian-brush (0.1-2) UNRELEASED; urgency=medium
2335
2336 [ Author One ]
2337 * Change by author one.
2338
2339 [ Author Two ]
2340 * Change by author two.
2341
2342 -- Jelmer Vernooij <jelmer@debian.org> Fri, 23 Nov 2018 14:00:02 +0000
2343"#,
2344 );
2345 }
2346
2347 #[test]
2348 fn test_remove_first_of_two_section_headers() {
2349 test_remove_change(
2351 r#"lintian-brush (0.1-2) UNRELEASED; urgency=medium
2352
2353 [ Author One ]
2354 * Change by author one.
2355
2356 [ Author Two ]
2357 * Change by author two.
2358
2359 -- Jelmer Vernooij <jelmer@debian.org> Fri, 23 Nov 2018 14:00:02 +0000
2360"#,
2361 |c| c.author() == Some("Author One"),
2362 r#"lintian-brush (0.1-2) UNRELEASED; urgency=medium
2363
2364 [ Author Two ]
2365 * Change by author two.
2366
2367 -- Jelmer Vernooij <jelmer@debian.org> Fri, 23 Nov 2018 14:00:02 +0000
2368"#,
2369 );
2370 }
2371
2372 #[test]
2373 fn test_remove_last_entry_no_empty_line_follows() {
2374 test_remove_change(
2376 r#"lintian-brush (0.1-2) UNRELEASED; urgency=medium
2377
2378 * First change.
2379 * Last change.
2380 -- Jelmer Vernooij <jelmer@debian.org> Fri, 23 Nov 2018 14:00:02 +0000
2381"#,
2382 |c| c.lines().iter().any(|l| l.contains("Last change")),
2383 r#"lintian-brush (0.1-2) UNRELEASED; urgency=medium
2384
2385 * First change.
2386 -- Jelmer Vernooij <jelmer@debian.org> Fri, 23 Nov 2018 14:00:02 +0000
2387"#,
2388 );
2389 }
2390
2391 #[test]
2392 fn test_remove_first_unattributed_before_section_exact() {
2393 let changelog: ChangeLog = r#"lintian-brush (0.1-2) UNRELEASED; urgency=medium
2396
2397 * Team upload.
2398
2399 [ Jelmer Vernooij ]
2400 * blah
2401
2402 -- Jelmer Vernooij <jelmer@debian.org> Fri, 23 Nov 2018 14:00:02 +0000
2403"#
2404 .parse()
2405 .unwrap();
2406
2407 let changes = iter_changes_by_author(&changelog);
2409 let team_upload_change = changes
2410 .iter()
2411 .find(|c| c.author().is_none() && c.lines().iter().any(|l| l.contains("Team upload")))
2412 .unwrap();
2413
2414 let bullets = team_upload_change.split_into_bullets();
2415 bullets[0].clone().remove();
2416
2417 let result = changelog.to_string();
2418
2419 let expected = r#"lintian-brush (0.1-2) UNRELEASED; urgency=medium
2421
2422 [ Jelmer Vernooij ]
2423 * blah
2424
2425 -- Jelmer Vernooij <jelmer@debian.org> Fri, 23 Nov 2018 14:00:02 +0000
2426"#;
2427 assert_eq!(result, expected);
2428 }
2429
2430 #[test]
2431 fn test_replace_with_preserves_first_blank_line() {
2432 let changelog: ChangeLog = r#"blah (2.6.0) unstable; urgency=medium
2435
2436 * New upstream release.
2437 * Fix blocks/blockedby of archived bugs (Closes: #XXXXXXX). Thanks to somebody who fixed it.
2438
2439 -- Joe Example <joe@example.com> Mon, 26 Feb 2018 11:31:48 -0800
2440"#
2441 .parse()
2442 .unwrap();
2443
2444 let changes = iter_changes_by_author(&changelog);
2445
2446 changes[0].replace_with(vec![
2448 "* New upstream release.",
2449 " * Fix blocks/blockedby of archived bugs (Closes: #XXXXXXX). Thanks to",
2450 " somebody who fixed it.",
2451 ]);
2452
2453 let result = changelog.to_string();
2454
2455 let expected = r#"blah (2.6.0) unstable; urgency=medium
2457
2458 * New upstream release.
2459 * Fix blocks/blockedby of archived bugs (Closes: #XXXXXXX). Thanks to
2460 somebody who fixed it.
2461
2462 -- Joe Example <joe@example.com> Mon, 26 Feb 2018 11:31:48 -0800
2463"#;
2464 assert_eq!(result, expected);
2465 }
2466
2467 #[test]
2468 fn test_parse_serialize_preserves_blank_line() {
2469 let input = r#"blah (2.6.0) unstable; urgency=medium
2471
2472 * New upstream release.
2473 * Fix blocks/blockedby of archived bugs (Closes: #XXXXXXX). Thanks to somebody who fixed it.
2474
2475 -- Joe Example <joe@example.com> Mon, 26 Feb 2018 11:31:48 -0800
2476"#;
2477
2478 let changelog: ChangeLog = input.parse().unwrap();
2479 let output = changelog.to_string();
2480
2481 assert_eq!(output, input, "Parse/serialize should not modify changelog");
2482 }
2483
2484 #[test]
2485 fn test_replace_with_first_entry_preserves_blank() {
2486 let changelog: ChangeLog = r#"blah (2.6.0) unstable; urgency=medium
2489
2490 * New upstream release.
2491 * Fix blocks/blockedby of archived bugs (Closes: #XXXXXXX). Thanks to somebody who fixed it.
2492
2493 -- Joe Example <joe@example.com> Mon, 26 Feb 2018 11:31:48 -0800
2494"#
2495 .parse()
2496 .unwrap();
2497
2498 let changes = iter_changes_by_author(&changelog);
2499 assert_eq!(changes.len(), 1);
2500
2501 changes[0].replace_with(vec![
2503 "* New upstream release.",
2504 " * Fix blocks/blockedby of archived bugs (Closes: #XXXXXXX). Thanks to",
2505 " somebody who fixed it.",
2506 ]);
2507
2508 let result = changelog.to_string();
2509
2510 let expected = r#"blah (2.6.0) unstable; urgency=medium
2512
2513 * New upstream release.
2514 * Fix blocks/blockedby of archived bugs (Closes: #XXXXXXX). Thanks to
2515 somebody who fixed it.
2516
2517 -- Joe Example <joe@example.com> Mon, 26 Feb 2018 11:31:48 -0800
2518"#;
2519 assert_eq!(result, expected);
2520 }
2521
2522 #[test]
2523 fn test_pop_append_preserves_first_blank() {
2524 let changelog: ChangeLog = r#"blah (2.6.0) unstable; urgency=medium
2527
2528 * New upstream release.
2529 * Fix blocks/blockedby of archived bugs (Closes: #XXXXXXX). Thanks to somebody who fixed it.
2530
2531 -- Joe Example <joe@example.com> Mon, 26 Feb 2018 11:31:48 -0800
2532"#
2533 .parse()
2534 .unwrap();
2535
2536 let entry = changelog.iter().next().unwrap();
2537
2538 while entry.pop_change_line().is_some() {}
2540
2541 entry.append_change_line("* New upstream release.");
2543 entry.append_change_line(
2544 " * Fix blocks/blockedby of archived bugs (Closes: #XXXXXXX). Thanks to",
2545 );
2546 entry.append_change_line(" somebody who fixed it.");
2547
2548 let result = changelog.to_string();
2549
2550 let expected = r#"blah (2.6.0) unstable; urgency=medium
2552
2553 * New upstream release.
2554 * Fix blocks/blockedby of archived bugs (Closes: #XXXXXXX). Thanks to
2555 somebody who fixed it.
2556
2557 -- Joe Example <joe@example.com> Mon, 26 Feb 2018 11:31:48 -0800
2558"#;
2559 assert_eq!(result, expected);
2560 }
2561
2562 #[test]
2563 fn test_replace_line() {
2564 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
2565
2566 * First change
2567 * Second change
2568 * Third change
2569
2570 -- Jelmer Vernooij <jelmer@debian.org> Mon, 04 Sep 2023 18:13:45 -0500
2571"#
2572 .parse()
2573 .unwrap();
2574
2575 let changes = iter_changes_by_author(&changelog);
2576 assert_eq!(changes.len(), 1);
2577
2578 changes[0]
2580 .replace_line(1, "* Updated second change")
2581 .unwrap();
2582
2583 let updated_changes = iter_changes_by_author(&changelog);
2585 assert_eq!(
2586 updated_changes[0].lines(),
2587 vec![
2588 "* First change",
2589 "* Updated second change",
2590 "* Third change"
2591 ]
2592 );
2593 }
2594
2595 #[test]
2596 fn test_replace_line_out_of_bounds() {
2597 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
2598
2599 * First change
2600
2601 -- Jelmer Vernooij <jelmer@debian.org> Mon, 04 Sep 2023 18:13:45 -0500
2602"#
2603 .parse()
2604 .unwrap();
2605
2606 let changes = iter_changes_by_author(&changelog);
2607 assert_eq!(changes.len(), 1);
2608
2609 let result = changes[0].replace_line(5, "* Updated");
2611 assert!(result.is_err());
2612 }
2613
2614 #[test]
2615 fn test_update_lines() {
2616 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
2617
2618 * First change
2619 * Second change
2620 * Third change
2621
2622 -- Jelmer Vernooij <jelmer@debian.org> Mon, 04 Sep 2023 18:13:45 -0500
2623"#
2624 .parse()
2625 .unwrap();
2626
2627 let changes = iter_changes_by_author(&changelog);
2628
2629 let count = changes[0].update_lines(
2631 |line| line.contains("First") || line.contains("Second"),
2632 |line| format!("{} (updated)", line),
2633 );
2634
2635 assert_eq!(count, 2);
2636
2637 let updated_changes = iter_changes_by_author(&changelog);
2639 assert_eq!(
2640 updated_changes[0].lines(),
2641 vec![
2642 "* First change (updated)",
2643 "* Second change (updated)",
2644 "* Third change"
2645 ]
2646 );
2647 }
2648
2649 #[test]
2650 fn test_update_lines_no_matches() {
2651 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
2652
2653 * First change
2654 * Second change
2655
2656 -- Jelmer Vernooij <jelmer@debian.org> Mon, 04 Sep 2023 18:13:45 -0500
2657"#
2658 .parse()
2659 .unwrap();
2660
2661 let changes = iter_changes_by_author(&changelog);
2662
2663 let count = changes[0].update_lines(
2665 |line| line.contains("NonExistent"),
2666 |line| format!("{} (updated)", line),
2667 );
2668
2669 assert_eq!(count, 0);
2670
2671 let updated_changes = iter_changes_by_author(&changelog);
2673 assert_eq!(
2674 updated_changes[0].lines(),
2675 vec!["* First change", "* Second change"]
2676 );
2677 }
2678
2679 #[test]
2680 fn test_update_lines_with_continuation() {
2681 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
2682
2683 * First change
2684 with continuation line
2685 * Second change
2686
2687 -- Jelmer Vernooij <jelmer@debian.org> Mon, 04 Sep 2023 18:13:45 -0500
2688"#
2689 .parse()
2690 .unwrap();
2691
2692 let changes = iter_changes_by_author(&changelog);
2693
2694 let count = changes[0].update_lines(
2696 |line| line.contains("continuation"),
2697 |line| line.replace("continuation", "updated"),
2698 );
2699
2700 assert_eq!(count, 1);
2701
2702 let updated_changes = iter_changes_by_author(&changelog);
2704 assert_eq!(
2705 updated_changes[0].lines(),
2706 vec!["* First change", " with updated line", "* Second change"]
2707 );
2708 }
2709
2710 #[test]
2711 fn test_add_bullet() {
2712 let mut changelog = ChangeLog::new();
2713 let entry = changelog
2714 .new_entry()
2715 .maintainer(("Test User".into(), "test@example.com".into()))
2716 .distribution("unstable".to_string())
2717 .version("1.0.0".parse().unwrap())
2718 .finish();
2719
2720 entry.add_bullet("First change");
2722 entry.add_bullet("Second change");
2723 entry.add_bullet("Third change");
2724
2725 let lines: Vec<_> = entry.change_lines().collect();
2726 assert_eq!(lines.len(), 3);
2727 assert_eq!(lines[0], "* First change");
2728 assert_eq!(lines[1], "* Second change");
2729 assert_eq!(lines[2], "* Third change");
2730 }
2731
2732 #[test]
2733 fn test_add_bullet_empty_entry() {
2734 let mut changelog = ChangeLog::new();
2735 let entry = changelog
2736 .new_entry()
2737 .maintainer(("Test User".into(), "test@example.com".into()))
2738 .distribution("unstable".to_string())
2739 .version("1.0.0".parse().unwrap())
2740 .finish();
2741
2742 entry.add_bullet("Only bullet");
2743
2744 let lines: Vec<_> = entry.change_lines().collect();
2745 assert_eq!(lines.len(), 1);
2746 assert_eq!(lines[0], "* Only bullet");
2747 }
2748
2749 #[test]
2750 fn test_add_bullet_long_text() {
2751 let mut changelog = ChangeLog::new();
2752 let entry = changelog
2753 .new_entry()
2754 .maintainer(("Test User".into(), "test@example.com".into()))
2755 .distribution("unstable".to_string())
2756 .version("1.0.0".parse().unwrap())
2757 .finish();
2758
2759 entry.add_bullet("This is a very long line that exceeds the 78 column limit and should be automatically wrapped to multiple lines with proper indentation");
2761
2762 let lines: Vec<_> = entry.change_lines().collect();
2763 assert!(lines.len() > 1);
2765 assert!(lines[0].starts_with("* "));
2767 for line in &lines[1..] {
2769 assert!(line.starts_with(" "));
2770 }
2771 for line in &lines {
2773 assert!(line.len() <= 78, "Line exceeds 78 chars: {}", line);
2774 }
2775 }
2776
2777 #[test]
2778 fn test_add_bullet_preserves_closes() {
2779 let mut changelog = ChangeLog::new();
2780 let entry = changelog
2781 .new_entry()
2782 .maintainer(("Test User".into(), "test@example.com".into()))
2783 .distribution("unstable".to_string())
2784 .version("1.0.0".parse().unwrap())
2785 .finish();
2786
2787 entry.add_bullet("Fix a very important bug that was causing problems (Closes: #123456)");
2789
2790 let lines: Vec<_> = entry.change_lines().collect();
2791 let text = lines.join(" ");
2792 assert!(text.contains("Closes: #123456"));
2794 }
2795
2796 #[test]
2797 fn test_add_bullet_multiple_closes() {
2798 let mut changelog = ChangeLog::new();
2799 let entry = changelog
2800 .new_entry()
2801 .maintainer(("Test User".into(), "test@example.com".into()))
2802 .distribution("unstable".to_string())
2803 .version("1.0.0".parse().unwrap())
2804 .finish();
2805
2806 entry.add_bullet("Fix several bugs (Closes: #123456, #789012)");
2808
2809 let lines: Vec<_> = entry.change_lines().collect();
2810 let text = lines.join(" ");
2811 assert!(text.contains("Closes: #123456"));
2812 assert!(text.contains("#789012"));
2813 }
2814
2815 #[test]
2816 fn test_add_bullet_preserves_lp() {
2817 let mut changelog = ChangeLog::new();
2818 let entry = changelog
2819 .new_entry()
2820 .maintainer(("Test User".into(), "test@example.com".into()))
2821 .distribution("unstable".to_string())
2822 .version("1.0.0".parse().unwrap())
2823 .finish();
2824
2825 entry.add_bullet("Fix bug (LP: #123456)");
2827
2828 let lines: Vec<_> = entry.change_lines().collect();
2829 let text = lines.join(" ");
2830 assert!(text.contains("LP: #123456"));
2832 }
2833
2834 #[test]
2835 fn test_add_bullet_with_existing_bullets() {
2836 let mut changelog = ChangeLog::new();
2837 let entry = changelog
2838 .new_entry()
2839 .maintainer(("Test User".into(), "test@example.com".into()))
2840 .distribution("unstable".to_string())
2841 .version("1.0.0".parse().unwrap())
2842 .change_line("* Existing change".to_string())
2843 .finish();
2844
2845 entry.add_bullet("New change");
2847
2848 let lines: Vec<_> = entry.change_lines().collect();
2849 assert_eq!(lines.len(), 2);
2850 assert_eq!(lines[0], "* Existing change");
2851 assert_eq!(lines[1], "* New change");
2852 }
2853
2854 #[test]
2855 fn test_add_bullet_special_characters() {
2856 let mut changelog = ChangeLog::new();
2857 let entry = changelog
2858 .new_entry()
2859 .maintainer(("Test User".into(), "test@example.com".into()))
2860 .distribution("unstable".to_string())
2861 .version("1.0.0".parse().unwrap())
2862 .finish();
2863
2864 entry.add_bullet("Fix issue with \"quotes\" and 'apostrophes'");
2865 entry.add_bullet("Handle paths like /usr/bin/foo");
2866 entry.add_bullet("Support $VARIABLES and ${EXPANSIONS}");
2867
2868 let lines: Vec<_> = entry.change_lines().collect();
2869 assert_eq!(lines.len(), 3);
2870 assert!(lines[0].contains("\"quotes\""));
2871 assert!(lines[1].contains("/usr/bin/foo"));
2872 assert!(lines[2].contains("$VARIABLES"));
2873 }
2874
2875 #[test]
2876 fn test_add_bullet_empty_string() {
2877 let mut changelog = ChangeLog::new();
2878 let entry = changelog
2879 .new_entry()
2880 .maintainer(("Test User".into(), "test@example.com".into()))
2881 .distribution("unstable".to_string())
2882 .version("1.0.0".parse().unwrap())
2883 .finish();
2884
2885 entry.add_bullet("");
2887
2888 let lines: Vec<_> = entry.change_lines().collect();
2889 assert_eq!(lines.len(), 0);
2891 }
2892
2893 #[test]
2894 fn test_add_bullet_url() {
2895 let mut changelog = ChangeLog::new();
2896 let entry = changelog
2897 .new_entry()
2898 .maintainer(("Test User".into(), "test@example.com".into()))
2899 .distribution("unstable".to_string())
2900 .version("1.0.0".parse().unwrap())
2901 .finish();
2902
2903 entry.add_bullet("Update documentation at https://www.example.com/very/long/path/to/documentation/page.html");
2905
2906 let lines: Vec<_> = entry.change_lines().collect();
2907 let text = lines.join(" ");
2908 assert!(text.contains("https://www.example.com"));
2909 }
2910
2911 #[test]
2912 fn test_add_bullet_mixed_with_manual_changes() {
2913 let mut changelog = ChangeLog::new();
2914 let entry = changelog
2915 .new_entry()
2916 .maintainer(("Test User".into(), "test@example.com".into()))
2917 .distribution("unstable".to_string())
2918 .version("1.0.0".parse().unwrap())
2919 .finish();
2920
2921 entry.add_bullet("First bullet");
2923 entry.append_change_line(" Manual continuation line");
2924 entry.add_bullet("Second bullet");
2925
2926 let lines: Vec<_> = entry.change_lines().collect();
2927 assert_eq!(lines.len(), 3);
2928 assert_eq!(lines[0], "* First bullet");
2929 assert_eq!(lines[1], " Manual continuation line");
2930 assert_eq!(lines[2], "* Second bullet");
2931 }
2932
2933 #[test]
2934 fn test_replace_line_with_continuation() {
2935 let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
2936
2937 * First change
2938 with continuation line
2939 * Second change
2940
2941 -- Jelmer Vernooij <jelmer@debian.org> Mon, 04 Sep 2023 18:13:45 -0500
2942"#
2943 .parse()
2944 .unwrap();
2945
2946 let changes = iter_changes_by_author(&changelog);
2947
2948 changes[0]
2950 .replace_line(1, " with updated continuation")
2951 .unwrap();
2952
2953 let updated_changes = iter_changes_by_author(&changelog);
2954 assert_eq!(
2955 updated_changes[0].lines(),
2956 vec![
2957 "* First change",
2958 " with updated continuation",
2959 "* Second change"
2960 ]
2961 );
2962 }
2963
2964 #[test]
2965 fn test_change_line_col() {
2966 let changelog: ChangeLog = r#"foo (1.0-1) unstable; urgency=low
2967
2968 * First change
2969 * Second change
2970
2971 -- Maintainer <email@example.com> Mon, 01 Jan 2024 12:00:00 +0000
2972
2973bar (2.0-1) experimental; urgency=high
2974
2975 [ Alice ]
2976 * Alice's change
2977 * Alice's second change
2978
2979 [ Bob ]
2980 * Bob's change
2981
2982 -- Another <another@example.com> Tue, 02 Jan 2024 13:00:00 +0000
2983"#
2984 .parse()
2985 .unwrap();
2986
2987 let changes = iter_changes_by_author(&changelog);
2988
2989 assert_eq!(changes.len(), 3);
2991
2992 assert_eq!(changes[0].line(), Some(2));
2994 assert_eq!(changes[0].column(), Some(2)); assert_eq!(changes[0].line_col(), Some((2, 2)));
2996 assert_eq!(changes[0].lines().len(), 2); assert_eq!(changes[1].line(), Some(10));
3000 assert_eq!(changes[1].column(), Some(2)); assert_eq!(changes[1].lines().len(), 2); assert_eq!(changes[2].line(), Some(14));
3005 assert_eq!(changes[2].column(), Some(2)); assert_eq!(changes[2].lines().len(), 1); }
3008}