debian_changelog/
parse.rs

1use crate::lex::lex;
2use crate::SyntaxKind;
3use crate::SyntaxKind::*;
4#[cfg(feature = "chrono")]
5use chrono::{DateTime, FixedOffset};
6use debversion::Version;
7use rowan::ast::AstNode;
8use std::str::FromStr;
9
10/// Trait for types that can be converted to a timestamp string
11///
12/// This trait allows both chrono DateTime types and plain strings to be used
13/// as timestamps in the changelog API.
14pub trait IntoTimestamp {
15    /// Convert this value into a timestamp string in Debian changelog format
16    fn into_timestamp(self) -> String;
17}
18
19impl IntoTimestamp for String {
20    fn into_timestamp(self) -> String {
21        self
22    }
23}
24
25impl IntoTimestamp for &str {
26    fn into_timestamp(self) -> String {
27        self.to_string()
28    }
29}
30
31#[cfg(feature = "chrono")]
32impl<Tz: chrono::TimeZone> IntoTimestamp for DateTime<Tz>
33where
34    Tz::Offset: std::fmt::Display,
35{
36    fn into_timestamp(self) -> String {
37        const CHANGELOG_TIME_FORMAT: &str = "%a, %d %b %Y %H:%M:%S %z";
38        self.format(CHANGELOG_TIME_FORMAT).to_string()
39    }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
43/// Urgency of the changes in the changelog entry
44pub enum Urgency {
45    #[default]
46    /// Low urgency
47    Low,
48
49    /// Medium urgency
50    Medium,
51
52    /// High urgency
53    High,
54
55    /// Emergency urgency
56    Emergency,
57
58    /// Critical urgency
59    Critical,
60}
61
62impl std::fmt::Display for Urgency {
63    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
64        match self {
65            Urgency::Low => f.write_str("low"),
66            Urgency::Medium => f.write_str("medium"),
67            Urgency::High => f.write_str("high"),
68            Urgency::Emergency => f.write_str("emergency"),
69            Urgency::Critical => f.write_str("critical"),
70        }
71    }
72}
73
74impl FromStr for Urgency {
75    type Err = ParseError;
76
77    fn from_str(s: &str) -> Result<Self, Self::Err> {
78        match s.to_lowercase().as_str() {
79            "low" => Ok(Urgency::Low),
80            "medium" => Ok(Urgency::Medium),
81            "high" => Ok(Urgency::High),
82            "emergency" => Ok(Urgency::Emergency),
83            "critical" => Ok(Urgency::Critical),
84            _ => Err(ParseError(vec![format!("invalid urgency: {}", s)])),
85        }
86    }
87}
88
89#[derive(Debug)]
90/// Error while reading a changelog file.
91pub enum Error {
92    /// I/O Error
93    Io(std::io::Error),
94
95    /// Parsing error
96    Parse(ParseError),
97}
98
99impl std::fmt::Display for Error {
100    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
101        match &self {
102            Error::Io(e) => write!(f, "IO error: {}", e),
103            Error::Parse(e) => write!(f, "Parse error: {}", e),
104        }
105    }
106}
107
108impl From<std::io::Error> for Error {
109    fn from(e: std::io::Error) -> Self {
110        Error::Io(e)
111    }
112}
113
114impl std::error::Error for Error {}
115
116#[derive(Debug, Clone, PartialEq, Eq, Hash)]
117/// Error while parsing
118pub struct ParseError(Vec<String>);
119
120impl std::fmt::Display for ParseError {
121    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
122        for err in &self.0 {
123            writeln!(f, "{}", err)?;
124        }
125        Ok(())
126    }
127}
128
129impl std::error::Error for ParseError {}
130
131impl From<ParseError> for Error {
132    fn from(e: ParseError) -> Self {
133        Error::Parse(e)
134    }
135}
136
137/// Second, implementing the `Language` trait teaches rowan to convert between
138/// these two SyntaxKind types, allowing for a nicer SyntaxNode API where
139/// "kinds" are values from our `enum SyntaxKind`, instead of plain u16 values.
140#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
141pub enum Lang {}
142impl rowan::Language for Lang {
143    type Kind = SyntaxKind;
144    fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind {
145        unsafe { std::mem::transmute::<u16, SyntaxKind>(raw.0) }
146    }
147    fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind {
148        kind.into()
149    }
150}
151
152/// GreenNode is an immutable tree, which is cheap to change,
153/// but doesn't contain offsets and parent pointers.
154use rowan::{GreenNode, GreenToken};
155
156/// You can construct GreenNodes by hand, but a builder
157/// is helpful for top-down parsers: it maintains a stack
158/// of currently in-progress nodes
159use rowan::GreenNodeBuilder;
160
161/// The result of parsing: a syntax tree and a collection of errors.
162///
163/// This type is designed to be stored in Salsa databases as it contains
164/// the thread-safe `GreenNode` instead of the non-thread-safe `SyntaxNode`.
165#[derive(Debug)]
166pub struct Parse<T> {
167    green: GreenNode,
168    errors: Vec<String>,
169    _ty: std::marker::PhantomData<T>,
170}
171
172// The T parameter is only used as a phantom type, so we can implement Clone and PartialEq
173// without requiring T to implement them
174impl<T> Clone for Parse<T> {
175    fn clone(&self) -> Self {
176        Parse {
177            green: self.green.clone(),
178            errors: self.errors.clone(),
179            _ty: std::marker::PhantomData,
180        }
181    }
182}
183
184impl<T> PartialEq for Parse<T> {
185    fn eq(&self, other: &Self) -> bool {
186        self.green == other.green && self.errors == other.errors
187    }
188}
189
190impl<T> Eq for Parse<T> {}
191
192// Implement Send + Sync since GreenNode is thread-safe
193unsafe impl<T> Send for Parse<T> {}
194unsafe impl<T> Sync for Parse<T> {}
195
196impl<T> Parse<T> {
197    /// Create a new Parse result from a GreenNode and errors
198    pub fn new(green: GreenNode, errors: Vec<String>) -> Self {
199        Parse {
200            green,
201            errors,
202            _ty: std::marker::PhantomData,
203        }
204    }
205
206    /// Get the green node (thread-safe representation)
207    pub fn green(&self) -> &GreenNode {
208        &self.green
209    }
210
211    /// Get the syntax errors
212    pub fn errors(&self) -> &[String] {
213        &self.errors
214    }
215
216    /// Check if there are any errors
217    pub fn ok(&self) -> bool {
218        self.errors.is_empty()
219    }
220
221    /// Convert to a Result, returning the tree if there are no errors
222    pub fn to_result(self) -> Result<T, ParseError>
223    where
224        T: AstNode<Language = Lang>,
225    {
226        if self.errors.is_empty() {
227            let node = SyntaxNode::new_root(self.green);
228            Ok(T::cast(node).expect("root node has wrong type"))
229        } else {
230            Err(ParseError(self.errors))
231        }
232    }
233
234    /// Convert to a Result, returning a mutable tree if there are no errors
235    pub fn to_mut_result(self) -> Result<T, ParseError>
236    where
237        T: AstNode<Language = Lang>,
238    {
239        if self.errors.is_empty() {
240            let node = SyntaxNode::new_root_mut(self.green);
241            Ok(T::cast(node).expect("root node has wrong type"))
242        } else {
243            Err(ParseError(self.errors))
244        }
245    }
246
247    /// Get the parsed syntax tree, panicking if there are errors
248    pub fn tree(&self) -> T
249    where
250        T: AstNode<Language = Lang>,
251    {
252        assert!(
253            self.errors.is_empty(),
254            "tried to get tree with errors: {:?}",
255            self.errors
256        );
257        let node = SyntaxNode::new_root(self.green.clone());
258        T::cast(node).expect("root node has wrong type")
259    }
260
261    /// Get the syntax node
262    pub fn syntax_node(&self) -> SyntaxNode {
263        SyntaxNode::new_root(self.green.clone())
264    }
265
266    /// Get a mutable parsed syntax tree, panicking if there are errors
267    pub fn tree_mut(&self) -> T
268    where
269        T: AstNode<Language = Lang>,
270    {
271        assert!(
272            self.errors.is_empty(),
273            "tried to get tree with errors: {:?}",
274            self.errors
275        );
276        let node = SyntaxNode::new_root_mut(self.green.clone());
277        T::cast(node).expect("root node has wrong type")
278    }
279}
280
281fn parse(text: &str) -> Parse<ChangeLog> {
282    struct Parser {
283        /// input tokens, including whitespace,
284        /// in *reverse* order.
285        tokens: Vec<(SyntaxKind, String)>,
286        /// the in-progress tree.
287        builder: GreenNodeBuilder<'static>,
288        /// the list of syntax errors we've accumulated
289        /// so far.
290        errors: Vec<String>,
291    }
292
293    impl Parser {
294        fn error(&mut self, msg: String) {
295            self.builder.start_node(ERROR.into());
296            if self.current().is_some() {
297                self.bump();
298            }
299            self.errors.push(msg);
300            self.builder.finish_node();
301        }
302
303        fn parse_entry_header(&mut self) {
304            self.builder.start_node(ENTRY_HEADER.into());
305            self.expect(IDENTIFIER);
306
307            self.skip_ws();
308
309            if self.current() == Some(NEWLINE) {
310                self.bump();
311                self.builder.finish_node();
312                return;
313            }
314
315            self.expect(VERSION);
316
317            self.skip_ws();
318
319            self.builder.start_node(DISTRIBUTIONS.into());
320            loop {
321                match self.current() {
322                    Some(IDENTIFIER) => self.bump(),
323                    Some(NEWLINE) => {
324                        self.bump();
325                        self.builder.finish_node();
326                        self.builder.finish_node();
327                        return;
328                    }
329                    Some(SEMICOLON) => {
330                        break;
331                    }
332                    _ => {
333                        self.error("expected distribution or semicolon".to_string());
334                        break;
335                    }
336                }
337                self.skip_ws();
338            }
339            self.builder.finish_node();
340
341            self.skip_ws();
342
343            self.builder.start_node(METADATA.into());
344            if self.current() == Some(SEMICOLON) {
345                self.bump();
346                loop {
347                    self.skip_ws();
348
349                    if self.current() == Some(NEWLINE) {
350                        break;
351                    }
352
353                    self.builder.start_node(METADATA_ENTRY.into());
354                    if self.current() == Some(IDENTIFIER) {
355                        self.builder.start_node(METADATA_KEY.into());
356                        self.bump();
357                        self.builder.finish_node();
358                    } else {
359                        self.error("expected metadata key".to_string());
360                        self.builder.finish_node();
361                        break;
362                    }
363
364                    if self.current() == Some(EQUALS) {
365                        self.bump();
366                    } else {
367                        self.error("expected equals".to_string());
368                        self.builder.finish_node();
369                        break;
370                    }
371
372                    if self.current() == Some(IDENTIFIER) {
373                        self.builder.start_node(METADATA_VALUE.into());
374                        self.bump();
375                        // Handle old-style metadata values that may contain spaces and multiple tokens
376                        // e.g., "closes=53715 56047 56607"
377                        loop {
378                            match (self.current(), self.next()) {
379                                // Stop if we see a new key=value pattern (IDENTIFIER followed by EQUALS)
380                                (Some(WHITESPACE), Some(IDENTIFIER)) => {
381                                    // Look further ahead to see if there's an EQUALS after the identifier
382                                    // If there is, this is a new metadata entry, so stop here
383                                    // Otherwise, consume the whitespace and identifier as part of the value
384                                    if self.tokens.len() >= 3 {
385                                        if let Some((kind, _)) =
386                                            self.tokens.get(self.tokens.len() - 3)
387                                        {
388                                            if *kind == EQUALS {
389                                                break; // Next token starts a new metadata entry
390                                            }
391                                        }
392                                    }
393                                    self.bump(); // consume whitespace
394                                }
395                                (Some(WHITESPACE), _) => self.bump(),
396                                (Some(IDENTIFIER), _) => self.bump(),
397                                _ => break,
398                            }
399                        }
400                        self.builder.finish_node();
401                    } else {
402                        self.error("expected metadata value".to_string());
403                        self.builder.finish_node();
404                        break;
405                    }
406                    self.builder.finish_node();
407
408                    // Skip comma separators (old-style format)
409                    self.skip_ws();
410                    if self.current() == Some(ERROR) {
411                        // Peek at the token text to see if it's a comma
412                        if let Some((_, text)) = self.tokens.last() {
413                            if text == "," {
414                                self.bump(); // consume the comma
415                                continue;
416                            }
417                        }
418                    }
419                }
420            } else if self.current() == Some(NEWLINE) {
421            } else {
422                self.error("expected semicolon or newline".to_string());
423            }
424            self.builder.finish_node();
425
426            self.expect(NEWLINE);
427            self.builder.finish_node();
428        }
429
430        fn parse_entry(&mut self) {
431            self.builder.start_node(ENTRY.into());
432            self.parse_entry_header();
433            loop {
434                match self
435                    .tokens
436                    .last()
437                    .map(|(kind, token)| (kind, token.as_str()))
438                {
439                    None => {
440                        // End of file - entry without footer is valid
441                        break;
442                    }
443                    // empty line
444                    Some((NEWLINE, _)) => {
445                        self.builder.start_node(EMPTY_LINE.into());
446                        self.bump();
447                        self.builder.finish_node();
448                    }
449                    // details
450                    Some((INDENT, "  ")) => {
451                        self.parse_entry_detail();
452                    }
453                    // footer
454                    Some((INDENT, " -- ")) => {
455                        self.parse_entry_footer();
456                        break;
457                    }
458                    _ => break,
459                }
460            }
461
462            self.builder.finish_node();
463        }
464
465        pub fn parse_entry_detail(&mut self) {
466            self.builder.start_node(ENTRY_BODY.into());
467            self.expect(INDENT);
468
469            match self.current() {
470                Some(DETAIL) => {
471                    self.bump();
472                }
473                Some(NEWLINE) => {}
474                _ => {
475                    self.error("expected detail".to_string());
476                }
477            }
478
479            self.expect(NEWLINE);
480            self.builder.finish_node();
481        }
482
483        pub fn parse_entry_footer(&mut self) {
484            self.builder.start_node(ENTRY_FOOTER.into());
485
486            if self.current() != Some(INDENT) {
487                self.error("expected indent".to_string());
488            } else {
489                let dashes = &self.tokens.last().unwrap().1;
490                if dashes != " -- " {
491                    self.error("expected --".to_string());
492                } else {
493                    self.bump();
494                }
495            }
496
497            self.builder.start_node(MAINTAINER.into());
498            while self.current() == Some(TEXT)
499                || (self.current() == Some(WHITESPACE) && self.next() != Some(EMAIL))
500            {
501                self.bump();
502            }
503            self.builder.finish_node();
504
505            if self.current().is_some() && self.current() != Some(NEWLINE) {
506                self.expect(WHITESPACE);
507            }
508
509            if self.current().is_some() && self.current() != Some(NEWLINE) {
510                self.expect(EMAIL);
511            }
512
513            if self.tokens.last().map(|(k, t)| (*k, t.as_str())) == Some((WHITESPACE, "  ")) {
514                self.bump();
515            } else if self.current() == Some(WHITESPACE) {
516                self.error("expected two spaces".to_string());
517            } else if self.current() == Some(NEWLINE) {
518                self.bump();
519                self.builder.finish_node();
520                return;
521            } else {
522                self.error(format!("expected whitespace, got {:?}", self.current()));
523            }
524
525            self.builder.start_node(TIMESTAMP.into());
526
527            loop {
528                if self.current() != Some(TEXT) && self.current() != Some(WHITESPACE) {
529                    break;
530                }
531                self.bump();
532            }
533            self.builder.finish_node();
534
535            self.expect(NEWLINE);
536            self.builder.finish_node();
537        }
538
539        fn parse(mut self) -> Parse<ChangeLog> {
540            self.builder.start_node(ROOT.into());
541            loop {
542                match self.current() {
543                    None => break,
544                    Some(NEWLINE) => {
545                        self.builder.start_node(EMPTY_LINE.into());
546                        self.bump();
547                        self.builder.finish_node();
548                    }
549                    Some(COMMENT) => {
550                        self.bump();
551                    }
552                    Some(IDENTIFIER) => {
553                        self.parse_entry();
554                    }
555                    t => {
556                        self.error(format!("unexpected token {:?}", t));
557                        break;
558                    }
559                }
560            }
561            // Close the root node.
562            self.builder.finish_node();
563
564            // Turn the builder into a GreenNode
565            Parse::new(self.builder.finish(), self.errors)
566        }
567        /// Advance one token, adding it to the current branch of the tree builder.
568        fn bump(&mut self) {
569            let (kind, text) = self.tokens.pop().unwrap();
570            self.builder.token(kind.into(), text.as_str());
571        }
572        /// Peek at the first unprocessed token
573        fn current(&self) -> Option<SyntaxKind> {
574            self.tokens.last().map(|(kind, _)| *kind)
575        }
576
577        fn next(&self) -> Option<SyntaxKind> {
578            self.tokens
579                .get(self.tokens.len() - 2)
580                .map(|(kind, _)| *kind)
581        }
582
583        fn expect(&mut self, expected: SyntaxKind) {
584            if self.current() != Some(expected) {
585                self.error(format!("expected {:?}, got {:?}", expected, self.current()));
586            } else {
587                self.bump();
588            }
589        }
590        fn skip_ws(&mut self) {
591            while self.current() == Some(WHITESPACE) {
592                self.bump()
593            }
594        }
595    }
596
597    let mut tokens = lex(text);
598    tokens.reverse();
599    Parser {
600        tokens,
601        builder: GreenNodeBuilder::new(),
602        errors: Vec::new(),
603    }
604    .parse()
605}
606
607// To work with the parse results we need a view into the
608// green tree - the Syntax tree.
609// It is also immutable, like a GreenNode,
610// but it contains parent pointers, offsets, and
611// has identity semantics.
612
613pub type SyntaxNode = rowan::SyntaxNode<Lang>;
614#[allow(unused)]
615pub type SyntaxToken = rowan::SyntaxToken<Lang>;
616#[allow(unused)]
617type SyntaxElement = rowan::NodeOrToken<SyntaxNode, SyntaxToken>;
618
619/// Calculate line and column (both 0-indexed) for the given offset in the tree.
620/// Column is measured in bytes from the start of the line.
621pub(crate) fn line_col_at_offset(node: &SyntaxNode, offset: rowan::TextSize) -> (usize, usize) {
622    let root = node.ancestors().last().unwrap_or_else(|| node.clone());
623    let mut line = 0;
624    let mut last_newline_offset = rowan::TextSize::from(0);
625
626    for element in root.preorder_with_tokens() {
627        if let rowan::WalkEvent::Enter(rowan::NodeOrToken::Token(token)) = element {
628            if token.text_range().start() >= offset {
629                break;
630            }
631
632            // Count newlines and track position of last one
633            for (idx, _) in token.text().match_indices('\n') {
634                line += 1;
635                last_newline_offset =
636                    token.text_range().start() + rowan::TextSize::from((idx + 1) as u32);
637            }
638        }
639    }
640
641    let column: usize = (offset - last_newline_offset).into();
642    (line, column)
643}
644
645macro_rules! ast_node {
646    ($ast:ident, $kind:ident) => {
647        #[derive(Debug, Clone, PartialEq, Eq, Hash)]
648        #[repr(transparent)]
649        /// A node in the changelog syntax tree.
650        pub struct $ast(SyntaxNode);
651
652        impl AstNode for $ast {
653            type Language = Lang;
654
655            fn can_cast(kind: SyntaxKind) -> bool {
656                kind == $kind
657            }
658
659            fn cast(syntax: SyntaxNode) -> Option<Self> {
660                if Self::can_cast(syntax.kind()) {
661                    Some(Self(syntax))
662                } else {
663                    None
664                }
665            }
666
667            fn syntax(&self) -> &SyntaxNode {
668                &self.0
669            }
670        }
671
672        impl $ast {
673            #[allow(dead_code)]
674            fn replace_root(&mut self, new_root: SyntaxNode) {
675                self.0 = Self::cast(new_root).unwrap().0;
676            }
677
678            /// Get the line number (0-indexed) where this node starts.
679            pub fn line(&self) -> usize {
680                line_col_at_offset(&self.0, self.0.text_range().start()).0
681            }
682
683            /// Get the column number (0-indexed, in bytes) where this node starts.
684            pub fn column(&self) -> usize {
685                line_col_at_offset(&self.0, self.0.text_range().start()).1
686            }
687
688            /// Get both line and column (0-indexed) where this node starts.
689            /// Returns (line, column) where column is measured in bytes from the start of the line.
690            pub fn line_col(&self) -> (usize, usize) {
691                line_col_at_offset(&self.0, self.0.text_range().start())
692            }
693        }
694
695        impl std::fmt::Display for $ast {
696            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
697                f.write_str(self.0.text().to_string().as_str())
698            }
699        }
700    };
701}
702
703ast_node!(ChangeLog, ROOT);
704ast_node!(Entry, ENTRY);
705ast_node!(EntryHeader, ENTRY_HEADER);
706ast_node!(EntryBody, ENTRY_BODY);
707ast_node!(EntryFooter, ENTRY_FOOTER);
708ast_node!(Maintainer, MAINTAINER);
709ast_node!(Timestamp, TIMESTAMP);
710ast_node!(MetadataEntry, METADATA_ENTRY);
711ast_node!(MetadataKey, METADATA_KEY);
712ast_node!(MetadataValue, METADATA_VALUE);
713
714impl MetadataEntry {
715    /// Returns the key of the metadata entry.
716    pub fn key(&self) -> Option<String> {
717        self.0
718            .children()
719            .find_map(MetadataKey::cast)
720            .map(|k| k.to_string())
721    }
722
723    /// Returns the value of the metadata entry.
724    pub fn value(&self) -> Option<String> {
725        self.0
726            .children()
727            .find_map(MetadataValue::cast)
728            .map(|k| k.to_string())
729    }
730
731    /// Sets the value of the metadata entry.
732    pub fn set_value(&mut self, value: &str) {
733        let node = self
734            .0
735            .children_with_tokens()
736            .find(|it| it.kind() == METADATA_VALUE);
737        let mut builder = GreenNodeBuilder::new();
738        builder.start_node(METADATA_VALUE.into());
739        builder.token(IDENTIFIER.into(), value);
740        builder.finish_node();
741        let root = SyntaxNode::new_root_mut(builder.finish());
742
743        let range = if let Some(node) = node {
744            node.index()..node.index() + 1
745        } else {
746            let count = self.0.children().count();
747            count..count
748        };
749
750        self.0.splice_children(range, vec![root.into()]);
751    }
752}
753
754/// A builder for a changelog entry.
755pub struct EntryBuilder {
756    root: SyntaxNode,
757    package: Option<String>,
758    version: Option<Version>,
759    distributions: Option<Vec<String>>,
760    urgency: Option<Urgency>,
761    maintainer: Option<(String, String)>,
762    timestamp_string: Option<String>,
763    change_lines: Vec<String>,
764}
765
766impl EntryBuilder {
767    /// Set the package name
768    #[must_use]
769    pub fn package(mut self, package: String) -> Self {
770        self.package = Some(package);
771        self
772    }
773
774    /// Set the package version
775    #[must_use]
776    pub fn version(mut self, version: Version) -> Self {
777        self.version = Some(version);
778        self
779    }
780
781    /// Set the distribution(s)
782    #[must_use]
783    pub fn distributions(mut self, distributions: Vec<String>) -> Self {
784        self.distributions = Some(distributions);
785        self
786    }
787
788    #[must_use]
789    pub fn distribution(mut self, distribution: String) -> Self {
790        self.distributions
791            .get_or_insert_with(Vec::new)
792            .push(distribution);
793        self
794    }
795
796    #[must_use]
797    pub fn urgency(mut self, urgency: Urgency) -> Self {
798        self.urgency = Some(urgency);
799        self
800    }
801
802    #[must_use]
803    pub fn maintainer(mut self, maintainer: (String, String)) -> Self {
804        self.maintainer = Some(maintainer);
805        self
806    }
807
808    /// Set the timestamp (accepts chrono::DateTime or String)
809    #[must_use]
810    pub fn datetime(mut self, timestamp: impl IntoTimestamp) -> Self {
811        self.timestamp_string = Some(timestamp.into_timestamp());
812        self
813    }
814
815    #[must_use]
816    pub fn change_line(mut self, line: String) -> Self {
817        self.change_lines.push(line);
818        self
819    }
820
821    pub fn verify(&self) -> Result<(), String> {
822        if self.package.is_none() {
823            return Err("package is required".to_string());
824        }
825        if self.version.is_none() {
826            return Err("version is required".to_string());
827        }
828        match self.distributions {
829            None => {
830                return Err("at least one distribution is required".to_string());
831            }
832            Some(ref distributions) => {
833                if distributions.is_empty() {
834                    return Err("at least one distribution is required".to_string());
835                }
836            }
837        }
838        if self.change_lines.is_empty() {
839            return Err("at least one change line is required".to_string());
840        }
841        Ok(())
842    }
843
844    fn metadata(&self) -> impl Iterator<Item = (String, String)> {
845        let mut ret = vec![];
846        if let Some(urgency) = self.urgency.as_ref() {
847            ret.push(("urgency".to_string(), urgency.to_string()));
848        }
849        ret.into_iter()
850    }
851
852    pub fn finish(self) -> Entry {
853        if self.root.children().find_map(Entry::cast).is_some() {
854            let mut builder = GreenNodeBuilder::new();
855            builder.start_node(EMPTY_LINE.into());
856            builder.token(NEWLINE.into(), "\n");
857            builder.finish_node();
858            let syntax = SyntaxNode::new_root_mut(builder.finish());
859            self.root.splice_children(0..0, vec![syntax.into()]);
860        }
861
862        let mut builder = GreenNodeBuilder::new();
863        builder.start_node(ENTRY.into());
864        builder.start_node(ENTRY_HEADER.into());
865        if let Some(package) = self.package.as_ref() {
866            builder.token(IDENTIFIER.into(), package.as_str());
867        }
868        if let Some(version) = self.version.as_ref() {
869            builder.token(WHITESPACE.into(), " ");
870            builder.token(VERSION.into(), format!("({})", version).as_str());
871        }
872        if let Some(distributions) = self.distributions.as_ref() {
873            builder.token(WHITESPACE.into(), " ");
874            builder.start_node(DISTRIBUTIONS.into());
875            let mut it = distributions.iter().peekable();
876            while it.peek().is_some() {
877                builder.token(IDENTIFIER.into(), it.next().unwrap());
878                if it.peek().is_some() {
879                    builder.token(WHITESPACE.into(), " ");
880                }
881            }
882            builder.finish_node(); // DISTRIBUTIONS
883        }
884        let mut metadata = self.metadata().peekable();
885        if metadata.peek().is_some() {
886            builder.token(SEMICOLON.into(), ";");
887            builder.token(WHITESPACE.into(), " ");
888            builder.start_node(METADATA.into());
889            for (key, value) in metadata {
890                builder.start_node(METADATA_ENTRY.into());
891                builder.start_node(METADATA_KEY.into());
892                builder.token(IDENTIFIER.into(), key.as_str());
893                builder.finish_node(); // METADATA_KEY
894                builder.token(EQUALS.into(), "=");
895                builder.start_node(METADATA_VALUE.into());
896                builder.token(METADATA_VALUE.into(), value.as_str());
897                builder.finish_node(); // METADATA_VALUE
898                builder.finish_node(); // METADATA_ENTRY
899            }
900            builder.finish_node(); // METADATA
901        }
902        builder.token(NEWLINE.into(), "\n");
903        builder.finish_node(); // ENTRY_HEADER
904
905        builder.start_node(EMPTY_LINE.into());
906        builder.token(NEWLINE.into(), "\n");
907        builder.finish_node(); // EMPTY_LINE
908
909        for line in self.change_lines {
910            builder.start_node(ENTRY_BODY.into());
911            builder.token(INDENT.into(), "  ");
912            builder.token(DETAIL.into(), line.as_str());
913            builder.token(NEWLINE.into(), "\n");
914            builder.finish_node(); // ENTRY_BODY
915        }
916
917        builder.start_node(EMPTY_LINE.into());
918        builder.token(NEWLINE.into(), "\n");
919        builder.finish_node(); // EMPTY_LINE
920
921        builder.start_node(ENTRY_FOOTER.into());
922        builder.token(INDENT.into(), " -- ");
923        if let Some(maintainer) = self.maintainer.as_ref() {
924            builder.start_node(MAINTAINER.into());
925            let mut it = maintainer.0.split(' ').peekable();
926            while let Some(p) = it.next() {
927                builder.token(TEXT.into(), p);
928                if it.peek().is_some() {
929                    builder.token(WHITESPACE.into(), " ");
930                }
931            }
932            builder.finish_node(); // MAINTAINER
933        }
934
935        if let Some(maintainer) = self.maintainer.as_ref() {
936            builder.token(WHITESPACE.into(), " ");
937            builder.token(EMAIL.into(), format!("<{}>", maintainer.1).as_str());
938        }
939
940        if let Some(timestamp) = self.timestamp_string.as_ref() {
941            builder.token(WHITESPACE.into(), "  ");
942
943            builder.start_node(TIMESTAMP.into());
944            let mut it = timestamp.split(' ').peekable();
945            while let Some(p) = it.next() {
946                builder.token(TEXT.into(), p);
947                if it.peek().is_some() {
948                    builder.token(WHITESPACE.into(), " ");
949                }
950            }
951            builder.finish_node(); // TIMESTAMP
952        }
953        builder.token(NEWLINE.into(), "\n");
954        builder.finish_node(); // ENTRY_FOOTER
955
956        builder.finish_node(); // ENTRY
957        let syntax = SyntaxNode::new_root_mut(builder.finish());
958        self.root.splice_children(0..0, vec![syntax.clone().into()]);
959        Entry(syntax)
960    }
961}
962
963impl IntoIterator for ChangeLog {
964    type Item = Entry;
965    type IntoIter = std::vec::IntoIter<Entry>;
966
967    fn into_iter(self) -> Self::IntoIter {
968        // TODO: This is inefficient
969        self.iter().collect::<Vec<_>>().into_iter()
970    }
971}
972
973fn replay(builder: &mut GreenNodeBuilder, node: SyntaxNode) {
974    builder.start_node(node.kind().into());
975    for child in node.children_with_tokens() {
976        match child {
977            SyntaxElement::Node(n) => replay(builder, n),
978            SyntaxElement::Token(t) => {
979                builder.token(t.kind().into(), t.text());
980            }
981        }
982    }
983    builder.finish_node();
984}
985
986impl FromIterator<Entry> for ChangeLog {
987    fn from_iter<T: IntoIterator<Item = Entry>>(iter: T) -> Self {
988        let mut builder = GreenNodeBuilder::new();
989        builder.start_node(ROOT.into());
990        for entry in iter {
991            replay(&mut builder, entry.0.clone());
992        }
993        builder.finish_node();
994        ChangeLog(SyntaxNode::new_root_mut(builder.finish()))
995    }
996}
997
998impl ChangeLog {
999    /// Create a new, empty changelog.
1000    pub fn new() -> ChangeLog {
1001        let mut builder = GreenNodeBuilder::new();
1002
1003        builder.start_node(ROOT.into());
1004        builder.finish_node();
1005
1006        let syntax = SyntaxNode::new_root_mut(builder.finish());
1007        ChangeLog(syntax)
1008    }
1009
1010    /// Parse changelog text, returning a Parse result
1011    pub fn parse(text: &str) -> Parse<ChangeLog> {
1012        parse(text)
1013    }
1014
1015    /// Returns an iterator over all entries in the changelog file.
1016    pub fn iter(&self) -> impl Iterator<Item = Entry> + '_ {
1017        self.0.children().filter_map(Entry::cast)
1018    }
1019
1020    /// Returns an iterator over all entries in the changelog file.
1021    #[deprecated(since = "0.2.0", note = "use `iter` instead")]
1022    pub fn entries(&self) -> impl Iterator<Item = Entry> + '_ {
1023        self.iter()
1024    }
1025
1026    /// Create a new, empty entry.
1027    pub fn new_empty_entry(&mut self) -> EntryBuilder {
1028        EntryBuilder {
1029            root: self.0.clone(),
1030            package: None,
1031            version: None,
1032            distributions: None,
1033            urgency: None,
1034            maintainer: None,
1035            timestamp_string: None,
1036            change_lines: vec![],
1037        }
1038    }
1039
1040    fn first_valid_entry(&self) -> Option<Entry> {
1041        self.iter().find(|entry| {
1042            entry.package().is_some() && entry.header().is_some() && entry.footer().is_some()
1043        })
1044    }
1045
1046    /// Return a builder for a new entry.
1047    pub fn new_entry(&mut self) -> EntryBuilder {
1048        let base_entry = self.first_valid_entry();
1049        let package = base_entry
1050            .as_ref()
1051            .and_then(|first_entry| first_entry.package());
1052        let mut version = base_entry
1053            .as_ref()
1054            .and_then(|first_entry| first_entry.version());
1055        if let Some(version) = version.as_mut() {
1056            version.increment_debian();
1057        }
1058        EntryBuilder {
1059            root: if self.0.is_mutable() {
1060                self.0.clone()
1061            } else {
1062                self.0.clone_for_update()
1063            },
1064            package,
1065            version,
1066            distributions: Some(vec![crate::UNRELEASED.into()]),
1067            urgency: Some(Urgency::default()),
1068            maintainer: crate::get_maintainer(),
1069            #[cfg(feature = "chrono")]
1070            timestamp_string: Some(chrono::Utc::now().into_timestamp()),
1071            #[cfg(not(feature = "chrono"))]
1072            timestamp_string: None,
1073            change_lines: vec![],
1074        }
1075    }
1076
1077    /// Add a change to the changelog.
1078    ///
1079    /// This will update the current changelog entry if it is considered
1080    /// unreleased. Otherwise, a new entry will be created.
1081    ///
1082    /// If there is an existing entry, the change will be added to the end of
1083    /// the entry. If the previous change was attributed to another author,
1084    /// a new section line ("[ Author Name ]") will be added as well.
1085    ///
1086    /// # Arguments
1087    /// * `change` - The change to add, e.g. &["* Fix a bug"]
1088    /// * `author` - The author of the change, e.g. ("John Doe", "john@example")
1089    ///
1090    /// # Errors
1091    ///
1092    /// Returns an error if text rewrapping fails.
1093    pub fn try_auto_add_change(
1094        &mut self,
1095        change: &[&str],
1096        author: (String, String),
1097        datetime: Option<impl IntoTimestamp>,
1098        urgency: Option<Urgency>,
1099    ) -> Result<Entry, crate::textwrap::Error> {
1100        match self.first_valid_entry() {
1101            Some(entry) if entry.is_unreleased() != Some(false) => {
1102                // Add to existing entry
1103                entry.try_add_change_for_author(change, author)?;
1104                // TODO: set timestamp to std::cmp::max(entry.timestamp(), datetime)
1105                // TODO: set urgency to std::cmp::max(entry.urgency(), urgency)
1106                Ok(entry)
1107            }
1108            Some(_entry) => {
1109                // Create new entry
1110                let mut builder = self.new_entry();
1111                builder = builder.maintainer(author);
1112                if let Some(datetime) = datetime {
1113                    builder = builder.datetime(datetime);
1114                }
1115                if let Some(urgency) = urgency {
1116                    builder = builder.urgency(urgency);
1117                }
1118                for change in change {
1119                    builder = builder.change_line(change.to_string());
1120                }
1121                Ok(builder.finish())
1122            }
1123            None => {
1124                panic!("No existing entries found in changelog");
1125            }
1126        }
1127    }
1128
1129    /// Automatically add a change to the changelog
1130    ///
1131    /// If there is an existing entry, the change will be added to the end of
1132    /// the entry. If the previous change was attributed to another author,
1133    /// a new section line ("[ Author Name ]") will be added as well.
1134    ///
1135    /// # Deprecated
1136    ///
1137    /// This function panics on errors. Use [`ChangeLog::try_auto_add_change`] instead for proper error handling.
1138    ///
1139    /// # Panics
1140    ///
1141    /// Panics if text rewrapping fails.
1142    ///
1143    /// # Arguments
1144    /// * `change` - The change to add, e.g. &["* Fix a bug"]
1145    /// * `author` - The author of the change, e.g. ("John Doe", "john@example")
1146    #[cfg(feature = "chrono")]
1147    #[deprecated(
1148        since = "0.2.10",
1149        note = "Use try_auto_add_change for proper error handling"
1150    )]
1151    pub fn auto_add_change(
1152        &mut self,
1153        change: &[&str],
1154        author: (String, String),
1155        datetime: Option<chrono::DateTime<chrono::FixedOffset>>,
1156        urgency: Option<Urgency>,
1157    ) -> Entry {
1158        self.try_auto_add_change(change, author, datetime, urgency)
1159            .unwrap()
1160    }
1161
1162    /// Pop the first entry from the changelog.
1163    pub fn pop_first(&mut self) -> Option<Entry> {
1164        let mut it = self.iter();
1165        if let Some(entry) = it.next() {
1166            // Drop trailing newlines
1167            while let Some(sibling) = entry.0.next_sibling() {
1168                if sibling.kind() == EMPTY_LINE {
1169                    sibling.detach();
1170                } else {
1171                    break;
1172                }
1173            }
1174            entry.0.detach();
1175            Some(entry)
1176        } else {
1177            None
1178        }
1179    }
1180
1181    /// Read a changelog file from a path
1182    pub fn read_path(path: impl AsRef<std::path::Path>) -> Result<ChangeLog, Error> {
1183        let mut file = std::fs::File::open(path)?;
1184        Self::read(&mut file)
1185    }
1186
1187    /// Read a changelog file from a reader
1188    pub fn read<R: std::io::Read>(mut r: R) -> Result<ChangeLog, Error> {
1189        let mut buf = String::new();
1190        r.read_to_string(&mut buf)?;
1191        Ok(buf.parse()?)
1192    }
1193
1194    /// Read a changelog file from a reader, allowing for syntax errors
1195    pub fn read_relaxed<R: std::io::Read>(mut r: R) -> Result<ChangeLog, Error> {
1196        let mut buf = String::new();
1197        r.read_to_string(&mut buf)?;
1198
1199        let parsed = parse(&buf);
1200        // For relaxed parsing, we ignore errors and return the tree anyway
1201        let node = SyntaxNode::new_root_mut(parsed.green().clone());
1202        Ok(ChangeLog::cast(node).expect("root node has wrong type"))
1203    }
1204
1205    /// Write the changelog to a writer
1206    pub fn write<W: std::io::Write>(&self, mut w: W) -> Result<(), Error> {
1207        let buf = self.to_string();
1208        w.write_all(buf.as_bytes())?;
1209        Ok(())
1210    }
1211
1212    /// Write the changelog to a path
1213    pub fn write_to_path(&self, p: &std::path::Path) -> Result<(), Error> {
1214        let f = std::fs::File::create(p)?;
1215        self.write(f)?;
1216        Ok(())
1217    }
1218
1219    /// Iterator over entries grouped by their maintainer (author).
1220    ///
1221    /// Returns an iterator over tuples of (maintainer_name, maintainer_email, Vec<Entry>)
1222    /// where entries with the same maintainer are grouped together.
1223    pub fn iter_by_author(&self) -> impl Iterator<Item = (String, String, Vec<Entry>)> + '_ {
1224        crate::iter_entries_by_author(self)
1225    }
1226
1227    /// Get all unique authors across all entries in the changelog.
1228    ///
1229    /// This includes both maintainers from entry footers and authors from [ Author Name ] sections.
1230    pub fn get_all_authors(&self) -> std::collections::HashSet<crate::Identity> {
1231        let mut authors = std::collections::HashSet::new();
1232
1233        // Add maintainers from entry footers
1234        for entry in self.iter() {
1235            if let Some(identity) = entry.get_maintainer_identity() {
1236                authors.insert(identity);
1237            }
1238        }
1239
1240        // Add authors from change sections
1241        for entry in self.iter() {
1242            for author_name in entry.get_authors() {
1243                // Create identity with empty email since we only have names from change sections
1244                authors.insert(crate::Identity::new(author_name, "".to_string()));
1245            }
1246        }
1247
1248        authors
1249    }
1250}
1251
1252impl Default for ChangeLog {
1253    fn default() -> Self {
1254        Self::new()
1255    }
1256}
1257
1258impl FromStr for ChangeLog {
1259    type Err = ParseError;
1260
1261    fn from_str(s: &str) -> Result<Self, Self::Err> {
1262        ChangeLog::parse(s).to_mut_result()
1263    }
1264}
1265
1266impl FromStr for Entry {
1267    type Err = ParseError;
1268
1269    fn from_str(s: &str) -> Result<Self, Self::Err> {
1270        let cl: ChangeLog = s.parse()?;
1271        let mut entries = cl.iter();
1272        let entry = entries
1273            .next()
1274            .ok_or_else(|| ParseError(vec!["no entries found".to_string()]))?;
1275        if entries.next().is_some() {
1276            return Err(ParseError(vec!["multiple entries found".to_string()]));
1277        }
1278        Ok(entry)
1279    }
1280}
1281
1282impl EntryHeader {
1283    /// Returns the version of the entry, returning an error if the version string is invalid.
1284    ///
1285    /// Returns:
1286    /// - `Some(Ok(version))` if a valid version is found
1287    /// - `Some(Err(err))` if a version token exists but cannot be parsed
1288    /// - `None` if no version token is present
1289    pub fn try_version(&self) -> Option<Result<Version, debversion::ParseError>> {
1290        self.0.children_with_tokens().find_map(|it| {
1291            if let Some(token) = it.as_token() {
1292                if token.kind() == VERSION {
1293                    let text = token.text()[1..token.text().len() - 1].to_string();
1294                    return Some(text.parse());
1295                }
1296            }
1297            None
1298        })
1299    }
1300
1301    /// Returns the version of the entry.
1302    ///
1303    /// Note: This method silently returns `None` if the version string is invalid.
1304    /// Consider using [`try_version`](Self::try_version) instead to handle parsing errors properly.
1305    pub fn version(&self) -> Option<Version> {
1306        self.try_version().and_then(|r| r.ok())
1307    }
1308
1309    /// Returns the package name of the entry.
1310    pub fn package(&self) -> Option<String> {
1311        self.0.children_with_tokens().find_map(|it| {
1312            if let Some(token) = it.as_token() {
1313                if token.kind() == IDENTIFIER {
1314                    return Some(token.text().to_string());
1315                }
1316            }
1317            None
1318        })
1319    }
1320
1321    /// Returns the distributions of the entry.
1322    pub fn distributions(&self) -> Option<Vec<String>> {
1323        let node = self.0.children().find(|it| it.kind() == DISTRIBUTIONS);
1324
1325        node.map(|node| {
1326            node.children_with_tokens()
1327                .filter_map(|it| {
1328                    if let Some(token) = it.as_token() {
1329                        if token.kind() == IDENTIFIER {
1330                            return Some(token.text().to_string());
1331                        }
1332                    }
1333                    None
1334                })
1335                .collect::<Vec<_>>()
1336        })
1337    }
1338
1339    /// Set distributions for the entry.
1340    pub fn set_distributions(&mut self, _distributions: Vec<String>) {
1341        let node = self
1342            .0
1343            .children_with_tokens()
1344            .find(|it| it.kind() == DISTRIBUTIONS);
1345        let mut builder = GreenNodeBuilder::new();
1346        builder.start_node(DISTRIBUTIONS.into());
1347        for (i, distribution) in _distributions.iter().enumerate() {
1348            if i > 0 {
1349                builder.token(WHITESPACE.into(), " ");
1350            }
1351            builder.token(IDENTIFIER.into(), distribution);
1352        }
1353        builder.finish_node();
1354
1355        let (range, green) = if let Some(node) = node {
1356            (
1357                node.index()..node.index() + 1,
1358                vec![builder.finish().into()],
1359            )
1360        } else if let Some(version) = self
1361            .0
1362            .children_with_tokens()
1363            .find(|it| it.kind() == VERSION)
1364        {
1365            (
1366                version.index()..version.index() + 1,
1367                vec![
1368                    GreenToken::new(WHITESPACE.into(), " ").into(),
1369                    builder.finish().into(),
1370                ],
1371            )
1372        } else if let Some(metadata) = self
1373            .0
1374            .children_with_tokens()
1375            .find(|it| it.kind() == METADATA)
1376        {
1377            (
1378                metadata.index() - 1..metadata.index() - 1,
1379                vec![
1380                    GreenToken::new(WHITESPACE.into(), " ").into(),
1381                    builder.finish().into(),
1382                ],
1383            )
1384        } else {
1385            (
1386                self.0.children().count()..self.0.children().count(),
1387                vec![
1388                    GreenToken::new(WHITESPACE.into(), " ").into(),
1389                    builder.finish().into(),
1390                ],
1391            )
1392        };
1393
1394        let new_root = SyntaxNode::new_root_mut(self.0.green().splice_children(range, green));
1395        self.replace_root(new_root);
1396    }
1397
1398    /// Set the version for the entry.
1399    pub fn set_version(&mut self, version: &Version) {
1400        // Find the version token
1401        let node = self
1402            .0
1403            .children_with_tokens()
1404            .find(|it| it.kind() == VERSION);
1405        let (range, green) = if let Some(token) = node {
1406            (
1407                token.index()..token.index() + 1,
1408                vec![GreenToken::new(VERSION.into(), &format!("({})", version)).into()],
1409            )
1410        } else {
1411            let index = self
1412                .0
1413                .children_with_tokens()
1414                .position(|it| it.kind() == IDENTIFIER)
1415                .unwrap_or(0);
1416            (
1417                index + 1..index + 1,
1418                vec![
1419                    GreenToken::new(WHITESPACE.into(), " ").into(),
1420                    GreenToken::new(VERSION.into(), &format!("({})", version)).into(),
1421                ],
1422            )
1423        };
1424        let new_root = SyntaxNode::new_root_mut(self.0.green().splice_children(range, green));
1425
1426        self.replace_root(new_root);
1427    }
1428
1429    /// Set the package name for the entry.
1430    pub fn set_package(&mut self, package: String) {
1431        let node = self
1432            .0
1433            .children_with_tokens()
1434            .find(|it| it.kind() == IDENTIFIER);
1435
1436        let new_root = if let Some(token) = node {
1437            SyntaxNode::new_root_mut(self.0.green().splice_children(
1438                token.index()..token.index() + 1,
1439                vec![GreenToken::new(IDENTIFIER.into(), &package).into()],
1440            ))
1441        } else {
1442            SyntaxNode::new_root_mut(self.0.green().splice_children(
1443                0..0,
1444                vec![
1445                    GreenToken::new(IDENTIFIER.into(), &package).into(),
1446                    GreenToken::new(WHITESPACE.into(), " ").into(),
1447                ],
1448            ))
1449        };
1450
1451        self.replace_root(new_root);
1452    }
1453
1454    /// Set extra metadata for the entry.
1455    pub fn set_metadata(&mut self, key: &str, value: &str) {
1456        // Find the appropriate metadata node
1457        if let Some(mut node) = self
1458            .metadata_nodes()
1459            .find(|it| it.key().map(|k| k == key).unwrap_or(false))
1460        {
1461            node.set_value(value);
1462        } else if let Some(metadata) = self
1463            .0
1464            .children_with_tokens()
1465            .find(|it| it.kind() == METADATA)
1466        {
1467            let mut builder = GreenNodeBuilder::new();
1468            builder.start_node(METADATA_ENTRY.into());
1469            builder.start_node(METADATA_KEY.into());
1470            builder.token(IDENTIFIER.into(), key);
1471            builder.finish_node();
1472            builder.token(EQUALS.into(), "=");
1473            builder.start_node(METADATA_VALUE.into());
1474            builder.token(IDENTIFIER.into(), value);
1475            builder.finish_node();
1476            builder.finish_node();
1477
1478            let metadata = metadata.as_node().unwrap();
1479
1480            let count = metadata.children_with_tokens().count();
1481            self.0.splice_children(
1482                metadata.index()..metadata.index() + 1,
1483                vec![SyntaxNode::new_root_mut(metadata.green().splice_children(
1484                    count..count,
1485                    vec![
1486                        GreenToken::new(WHITESPACE.into(), " ").into(),
1487                        builder.finish().into(),
1488                    ],
1489                ))
1490                .into()],
1491            );
1492        } else {
1493            let mut builder = GreenNodeBuilder::new();
1494            builder.start_node(METADATA.into());
1495            builder.token(SEMICOLON.into(), ";");
1496            builder.token(WHITESPACE.into(), " ");
1497            builder.start_node(METADATA_ENTRY.into());
1498            builder.start_node(METADATA_KEY.into());
1499            builder.token(IDENTIFIER.into(), key);
1500            builder.finish_node();
1501            builder.token(EQUALS.into(), "=");
1502            builder.start_node(METADATA_VALUE.into());
1503            builder.token(IDENTIFIER.into(), value);
1504            builder.finish_node();
1505            builder.finish_node();
1506
1507            let new_root = SyntaxNode::new_root_mut(builder.finish());
1508
1509            // Add either just after DISTRIBUTIONS
1510            if let Some(distributions) = self
1511                .0
1512                .children_with_tokens()
1513                .find(|it| it.kind() == DISTRIBUTIONS)
1514            {
1515                self.0.splice_children(
1516                    distributions.index() + 1..distributions.index() + 1,
1517                    vec![new_root.into()],
1518                );
1519            } else if let Some(nl) = self
1520                .0
1521                .children_with_tokens()
1522                .find(|it| it.kind() == NEWLINE)
1523            {
1524                // Just before the newline
1525                self.0
1526                    .splice_children(nl.index()..nl.index(), vec![new_root.into()]);
1527            } else {
1528                let count = self.0.children_with_tokens().count();
1529                self.0.splice_children(count..count, vec![new_root.into()]);
1530            }
1531        }
1532    }
1533
1534    /// Returns an iterator over the metadata entry AST nodes.
1535    pub fn metadata_nodes(&self) -> impl Iterator<Item = MetadataEntry> + '_ {
1536        let node = self.0.children().find(|it| it.kind() == METADATA);
1537
1538        node.into_iter().flat_map(|node| {
1539            node.children_with_tokens()
1540                .filter_map(|it| MetadataEntry::cast(it.into_node()?))
1541        })
1542    }
1543
1544    /// Returns an iterator over the metadata key-value pairs.
1545    pub fn metadata(&self) -> impl Iterator<Item = (String, String)> + '_ {
1546        self.metadata_nodes().filter_map(|entry| {
1547            if let (Some(key), Some(value)) = (entry.key(), entry.value()) {
1548                Some((key, value))
1549            } else {
1550                None
1551            }
1552        })
1553    }
1554
1555    /// Returns the urgency of the entry.3
1556    pub fn urgency(&self) -> Option<Urgency> {
1557        for (key, value) in self.metadata() {
1558            if key.as_str() == "urgency" {
1559                return Some(value.parse().unwrap());
1560            }
1561        }
1562        None
1563    }
1564}
1565
1566impl EntryFooter {
1567    /// Returns the email address of the maintainer from the footer.
1568    pub fn email(&self) -> Option<String> {
1569        self.0.children_with_tokens().find_map(|it| {
1570            if let Some(token) = it.as_token() {
1571                let text = token.text();
1572                if token.kind() == EMAIL {
1573                    return Some(text[1..text.len() - 1].to_string());
1574                }
1575            }
1576            None
1577        })
1578    }
1579
1580    /// Returns the maintainer name from the footer.
1581    pub fn maintainer(&self) -> Option<String> {
1582        self.0
1583            .children()
1584            .find_map(Maintainer::cast)
1585            .map(|m| m.text())
1586            .filter(|s| !s.is_empty())
1587    }
1588
1589    /// Set the maintainer for the entry.
1590    pub fn set_maintainer(&mut self, maintainer: String) {
1591        let node = self
1592            .0
1593            .children_with_tokens()
1594            .find(|it| it.kind() == MAINTAINER);
1595        let new_root = if let Some(node) = node {
1596            SyntaxNode::new_root_mut(self.0.green().splice_children(
1597                node.index()..node.index() + 1,
1598                vec![GreenToken::new(MAINTAINER.into(), &maintainer).into()],
1599            ))
1600        } else if let Some(node) = self.0.children_with_tokens().find(|it| it.kind() == INDENT) {
1601            SyntaxNode::new_root_mut(self.0.green().splice_children(
1602                node.index() + 1..node.index() + 1,
1603                vec![GreenToken::new(MAINTAINER.into(), &maintainer).into()],
1604            ))
1605        } else {
1606            SyntaxNode::new_root_mut(self.0.green().splice_children(
1607                0..0,
1608                vec![
1609                    GreenToken::new(INDENT.into(), " -- ").into(),
1610                    GreenToken::new(MAINTAINER.into(), &maintainer).into(),
1611                ],
1612            ))
1613        };
1614
1615        self.replace_root(new_root);
1616    }
1617
1618    /// Set email for the entry.
1619    pub fn set_email(&mut self, _email: String) {
1620        let node = self.0.children_with_tokens().find(|it| it.kind() == EMAIL);
1621        let new_root = if let Some(node) = node {
1622            SyntaxNode::new_root_mut(self.0.green().splice_children(
1623                node.index()..node.index() + 1,
1624                vec![GreenToken::new(EMAIL.into(), &format!("<{}>", _email)).into()],
1625            ))
1626        } else if let Some(node) = self
1627            .0
1628            .children_with_tokens()
1629            .find(|it| it.kind() == MAINTAINER)
1630        {
1631            SyntaxNode::new_root_mut(self.0.green().splice_children(
1632                node.index() + 1..node.index() + 1,
1633                vec![GreenToken::new(EMAIL.into(), &format!("<{}>", _email)).into()],
1634            ))
1635        } else if let Some(node) = self.0.children_with_tokens().find(|it| it.kind() == INDENT) {
1636            SyntaxNode::new_root_mut(self.0.green().splice_children(
1637                node.index() + 1..node.index() + 1,
1638                vec![
1639                    GreenToken::new(MAINTAINER.into(), "").into(),
1640                    GreenToken::new(WHITESPACE.into(), " ").into(),
1641                    GreenToken::new(EMAIL.into(), &format!("<{}>", _email)).into(),
1642                ],
1643            ))
1644        } else {
1645            SyntaxNode::new_root_mut(self.0.green().splice_children(
1646                0..0,
1647                vec![
1648                    GreenToken::new(INDENT.into(), " -- ").into(),
1649                    GreenToken::new(MAINTAINER.into(), "").into(),
1650                    GreenToken::new(WHITESPACE.into(), " ").into(),
1651                    GreenToken::new(EMAIL.into(), &format!("<{}>", _email)).into(),
1652                ],
1653            ))
1654        };
1655
1656        self.replace_root(new_root);
1657    }
1658
1659    /// Returns the timestamp from the footer.
1660    pub fn timestamp(&self) -> Option<String> {
1661        self.0
1662            .children()
1663            .find_map(Timestamp::cast)
1664            .map(|m| m.text())
1665    }
1666
1667    /// Set timestamp for the entry.
1668    pub fn set_timestamp(&mut self, timestamp: String) {
1669        let node = self
1670            .0
1671            .children_with_tokens()
1672            .find(|it| it.kind() == TIMESTAMP);
1673        let new_root = if let Some(node) = node {
1674            SyntaxNode::new_root_mut(self.0.green().splice_children(
1675                node.index()..node.index() + 1,
1676                vec![GreenToken::new(TIMESTAMP.into(), &timestamp).into()],
1677            ))
1678        } else if let Some(node) = self.0.children_with_tokens().find(|it| it.kind() == INDENT) {
1679            SyntaxNode::new_root_mut(self.0.green().splice_children(
1680                node.index() + 1..node.index() + 1,
1681                vec![GreenToken::new(TIMESTAMP.into(), &timestamp).into()],
1682            ))
1683        } else if let Some(node) = self.0.children_with_tokens().find(|it| it.kind() == EMAIL) {
1684            SyntaxNode::new_root_mut(self.0.green().splice_children(
1685                node.index() + 1..node.index() + 1,
1686                vec![GreenToken::new(TIMESTAMP.into(), &timestamp).into()],
1687            ))
1688        } else {
1689            SyntaxNode::new_root_mut(self.0.green().splice_children(
1690                0..0,
1691                vec![
1692                    GreenToken::new(INDENT.into(), " -- ").into(),
1693                    GreenToken::new(TIMESTAMP.into(), &timestamp).into(),
1694                ],
1695            ))
1696        };
1697
1698        self.replace_root(new_root);
1699    }
1700}
1701
1702impl EntryBody {
1703    fn text(&self) -> String {
1704        self.0
1705            .children_with_tokens()
1706            .filter_map(|it| {
1707                if let Some(token) = it.as_token() {
1708                    if token.kind() == DETAIL {
1709                        return Some(token.text().to_string());
1710                    }
1711                }
1712                None
1713            })
1714            .collect::<Vec<_>>()
1715            .concat()
1716    }
1717}
1718
1719impl Timestamp {
1720    fn text(&self) -> String {
1721        self.0.text().to_string()
1722    }
1723}
1724
1725impl Maintainer {
1726    fn text(&self) -> String {
1727        self.0.text().to_string()
1728    }
1729}
1730
1731impl Entry {
1732    /// Returns the header AST node of the entry.
1733    pub fn header(&self) -> Option<EntryHeader> {
1734        self.0.children().find_map(EntryHeader::cast)
1735    }
1736
1737    /// Returns the body AST node of the entry.
1738    pub fn body(&self) -> Option<EntryBody> {
1739        self.0.children().find_map(EntryBody::cast)
1740    }
1741
1742    /// Returns the footer AST node of the entry.
1743    pub fn footer(&self) -> Option<EntryFooter> {
1744        self.0.children().find_map(EntryFooter::cast)
1745    }
1746
1747    /// Return the package name of the entry.
1748    pub fn package(&self) -> Option<String> {
1749        self.header().and_then(|h| h.package())
1750    }
1751
1752    /// Set the package name of the entry.
1753    pub fn set_package(&mut self, package: String) {
1754        if let Some(mut header) = self.header() {
1755            let header_index = header.0.index();
1756            header.set_package(package);
1757            self.0
1758                .splice_children(header_index..header_index + 1, vec![header.0.into()]);
1759        } else {
1760            self.create_header().set_package(package);
1761        }
1762    }
1763
1764    /// Returns the version of the entry, returning an error if the version string is invalid.
1765    ///
1766    /// Returns:
1767    /// - `Some(Ok(version))` if a valid version is found
1768    /// - `Some(Err(err))` if a version token exists but cannot be parsed
1769    /// - `None` if no version token is present or no header exists
1770    pub fn try_version(&self) -> Option<Result<Version, debversion::ParseError>> {
1771        self.header().and_then(|h| h.try_version())
1772    }
1773
1774    /// Returns the version of the entry.
1775    ///
1776    /// Note: This method silently returns `None` if the version string is invalid.
1777    /// Consider using [`try_version`](Self::try_version) instead to handle parsing errors properly.
1778    pub fn version(&self) -> Option<Version> {
1779        self.try_version().and_then(|r| r.ok())
1780    }
1781
1782    /// Set the version of the entry.
1783    pub fn set_version(&mut self, version: &Version) {
1784        if let Some(mut header) = self.header() {
1785            let header_index = header.0.index();
1786            header.set_version(version);
1787            self.0
1788                .splice_children(header_index..header_index + 1, vec![header.0.into()]);
1789        } else {
1790            self.create_header().set_version(version);
1791        }
1792    }
1793
1794    /// Return the distributions of the entry.
1795    pub fn distributions(&self) -> Option<Vec<String>> {
1796        self.header().and_then(|h| h.distributions())
1797    }
1798
1799    /// Set the distributions for the entry
1800    pub fn set_distributions(&mut self, distributions: Vec<String>) {
1801        if let Some(mut header) = self.header() {
1802            let header_index = header.0.index();
1803            header.set_distributions(distributions);
1804            self.0
1805                .splice_children(header_index..header_index + 1, vec![header.0.into()]);
1806        } else {
1807            self.create_header().set_distributions(distributions);
1808        }
1809    }
1810
1811    /// Returns the email address of the maintainer.
1812    pub fn email(&self) -> Option<String> {
1813        self.footer().and_then(|f| f.email())
1814    }
1815
1816    /// Returns the maintainer AST node.
1817    pub fn maintainer_node(&self) -> Option<Maintainer> {
1818        self.footer()
1819            .and_then(|f| f.0.children().find_map(Maintainer::cast))
1820    }
1821
1822    /// Returns the name of the maintainer.
1823    pub fn maintainer(&self) -> Option<String> {
1824        self.footer().and_then(|f| f.maintainer())
1825    }
1826
1827    /// Set the maintainer of the entry.
1828    pub fn set_maintainer(&mut self, maintainer: (String, String)) {
1829        if let Some(mut footer) = self.footer() {
1830            let footer_index = footer.0.index();
1831            footer.set_maintainer(maintainer.0);
1832            footer.set_email(maintainer.1);
1833            self.0
1834                .splice_children(footer_index..footer_index + 1, vec![footer.0.into()]);
1835        } else {
1836            let mut footer = self.create_footer();
1837            footer.set_maintainer(maintainer.0);
1838            footer.set_email(maintainer.1);
1839        }
1840    }
1841
1842    /// Returns the timestamp AST node.
1843    pub fn timestamp_node(&self) -> Option<Timestamp> {
1844        self.footer()
1845            .and_then(|f| f.0.children().find_map(Timestamp::cast))
1846    }
1847
1848    /// Returns the timestamp of the entry, as the raw string.
1849    pub fn timestamp(&self) -> Option<String> {
1850        self.footer().and_then(|f| f.timestamp())
1851    }
1852
1853    /// Set the timestamp of the entry.
1854    pub fn set_timestamp(&mut self, timestamp: String) {
1855        if let Some(mut footer) = self.footer() {
1856            let footer_index = footer.0.index();
1857            footer.set_timestamp(timestamp);
1858            self.0
1859                .splice_children(footer_index..footer_index + 1, vec![footer.0.into()]);
1860        } else {
1861            self.create_footer().set_timestamp(timestamp);
1862        }
1863    }
1864
1865    /// Set the datetime of the entry.
1866    #[cfg(feature = "chrono")]
1867    pub fn set_datetime(&mut self, datetime: DateTime<FixedOffset>) {
1868        self.set_timestamp(format!("{}", datetime.format("%a, %d %b %Y %H:%M:%S %z")));
1869    }
1870
1871    /// Returns the datetime of the entry.
1872    #[cfg(feature = "chrono")]
1873    pub fn datetime(&self) -> Option<DateTime<FixedOffset>> {
1874        self.timestamp().and_then(|ts| parse_time_string(&ts).ok())
1875    }
1876
1877    /// Returns the urgency of the entry.
1878    pub fn urgency(&self) -> Option<Urgency> {
1879        self.header().and_then(|h| h.urgency())
1880    }
1881
1882    fn create_header(&self) -> EntryHeader {
1883        let mut builder = GreenNodeBuilder::new();
1884        builder.start_node(ENTRY_HEADER.into());
1885        builder.token(NEWLINE.into(), "\n");
1886        builder.finish_node();
1887        let syntax = SyntaxNode::new_root_mut(builder.finish());
1888        self.0.splice_children(0..0, vec![syntax.into()]);
1889        EntryHeader(self.0.children().next().unwrap().clone_for_update())
1890    }
1891
1892    fn create_footer(&self) -> EntryFooter {
1893        let mut builder = GreenNodeBuilder::new();
1894        builder.start_node(ENTRY_FOOTER.into());
1895        builder.token(NEWLINE.into(), "\n");
1896        builder.finish_node();
1897        let syntax = SyntaxNode::new_root_mut(builder.finish());
1898        let count = self.0.children().count();
1899        self.0.splice_children(count..count, vec![syntax.into()]);
1900        EntryFooter(self.0.children().last().unwrap().clone_for_update())
1901    }
1902
1903    /// Set the urgency of the entry.
1904    pub fn set_urgency(&mut self, urgency: Urgency) {
1905        self.set_metadata("urgency", urgency.to_string().as_str());
1906    }
1907
1908    /// Set a metadata key-value pair for the entry.
1909    pub fn set_metadata(&mut self, key: &str, value: &str) {
1910        if let Some(mut header) = self.header() {
1911            let header_index = header.0.index();
1912            header.set_metadata(key, value);
1913            self.0
1914                .splice_children(header_index..header_index + 1, vec![header.0.into()]);
1915        } else {
1916            self.create_header().set_metadata(key, value);
1917        }
1918    }
1919
1920    /// Add a change for the specified author
1921    ///
1922    /// If the author is not the same as the current maintainer, a new
1923    /// section will be created for the author in the entry (e.g. "[ John Doe ]").
1924    ///
1925    /// Returns an error if text rewrapping fails.
1926    pub fn try_add_change_for_author(
1927        &self,
1928        change: &[&str],
1929        author: (String, String),
1930    ) -> Result<(), crate::textwrap::Error> {
1931        let changes_lines = self.change_lines().collect::<Vec<_>>();
1932        let by_author = crate::changes::changes_by_author(changes_lines.iter().map(|s| s.as_str()))
1933            .collect::<Vec<_>>();
1934
1935        // There are no per author sections yet, so attribute current changes to changelog entry author
1936        if by_author.iter().all(|(a, _, _)| a.is_none()) {
1937            if let Some(maintainer_name) = self.maintainer() {
1938                if author.0 != maintainer_name {
1939                    self.prepend_change_line(
1940                        crate::changes::format_section_title(maintainer_name.as_str()).as_str(),
1941                    );
1942                    if !self.change_lines().last().unwrap().is_empty() {
1943                        self.append_change_line("");
1944                    }
1945                    self.append_change_line(
1946                        crate::changes::format_section_title(author.0.as_str()).as_str(),
1947                    );
1948                }
1949            }
1950        } else if let Some(last_section) = by_author.last().as_ref() {
1951            if last_section.0 != Some(author.0.as_str()) {
1952                self.append_change_line("");
1953                self.append_change_line(
1954                    crate::changes::format_section_title(author.0.as_str()).as_str(),
1955                );
1956            }
1957        }
1958
1959        if let Some(last) = self.change_lines().last() {
1960            if last.trim().is_empty() {
1961                self.pop_change_line();
1962            }
1963        }
1964
1965        for line in crate::textwrap::try_rewrap_changes(change.iter().copied())? {
1966            self.append_change_line(line.as_ref());
1967        }
1968        Ok(())
1969    }
1970
1971    /// Add a change for the specified author
1972    ///
1973    /// If the author is not the same as the current maintainer, a new
1974    /// section will be created for the author in the entry (e.g. "[ John Doe ]").
1975    ///
1976    /// # Deprecated
1977    ///
1978    /// This function panics on errors. Use [`Entry::try_add_change_for_author`] instead for proper error handling.
1979    ///
1980    /// # Panics
1981    ///
1982    /// Panics if text rewrapping fails.
1983    #[deprecated(
1984        since = "0.2.10",
1985        note = "Use try_add_change_for_author for proper error handling"
1986    )]
1987    pub fn add_change_for_author(&self, change: &[&str], author: (String, String)) {
1988        self.try_add_change_for_author(change, author).unwrap()
1989    }
1990
1991    /// Prepend a change line to the entry
1992    pub fn prepend_change_line(&self, line: &str) {
1993        let mut builder = GreenNodeBuilder::new();
1994        builder.start_node(ENTRY_BODY.into());
1995        if !line.is_empty() {
1996            builder.token(INDENT.into(), "  ");
1997            builder.token(DETAIL.into(), line);
1998        }
1999        builder.token(NEWLINE.into(), "\n");
2000        builder.finish_node();
2001
2002        // Insert just after the header
2003        let mut it = self.0.children();
2004        let header = it.find(|n| n.kind() == ENTRY_HEADER);
2005
2006        let previous_line = it.find(|n| n.kind() == EMPTY_LINE).or(header);
2007
2008        let index = previous_line.map_or(0, |l| l.index() + 1);
2009
2010        let syntax = SyntaxNode::new_root_mut(builder.finish());
2011
2012        self.0.splice_children(index..index, vec![syntax.into()]);
2013    }
2014
2015    /// Pop the last change line from the entry
2016    pub fn pop_change_line(&self) -> Option<String> {
2017        // Find the last child of type ENTRY_BODY
2018        let last_child = self.0.children().filter(|n| n.kind() == ENTRY_BODY).last();
2019
2020        if let Some(last_child) = last_child {
2021            let text = last_child.children_with_tokens().find_map(|it| {
2022                if let Some(token) = it.as_token() {
2023                    if token.kind() == DETAIL {
2024                        return Some(token.text().to_string());
2025                    }
2026                }
2027                None
2028            });
2029            self.0
2030                .splice_children(last_child.index()..last_child.index() + 1, vec![]);
2031            text
2032        } else {
2033            None
2034        }
2035    }
2036
2037    /// Append a line to the changelog entry
2038    pub fn append_change_line(&self, line: &str) {
2039        let mut builder = GreenNodeBuilder::new();
2040        builder.start_node(ENTRY_BODY.into());
2041        if !line.is_empty() {
2042            builder.token(INDENT.into(), "  ");
2043            builder.token(DETAIL.into(), line);
2044        }
2045        builder.token(NEWLINE.into(), "\n");
2046        builder.finish_node();
2047
2048        // Find the last child of type ENTRY_BODY
2049        let last_child = self
2050            .0
2051            .children()
2052            .filter(|n| n.kind() == ENTRY_BODY)
2053            .last()
2054            .unwrap_or_else(|| {
2055                // No ENTRY_BODY nodes exist. Insert after the EMPTY_LINE that follows
2056                // the ENTRY_HEADER (if it exists), to preserve required blank line.
2057                let children: Vec<_> = self.0.children().collect();
2058                if children.len() >= 2
2059                    && children[0].kind() == ENTRY_HEADER
2060                    && children[1].kind() == EMPTY_LINE
2061                {
2062                    children[1].clone()
2063                } else {
2064                    children[0].clone()
2065                }
2066            });
2067
2068        let syntax = SyntaxNode::new_root_mut(builder.finish()).into();
2069        self.0
2070            .splice_children(last_child.index() + 1..last_child.index() + 1, vec![syntax]);
2071    }
2072
2073    /// Add a bullet point to the changelog entry.
2074    ///
2075    /// This is a convenience method that appends a bullet point line to the entry.
2076    /// Always prepends "* " to the text and wraps the text to 78 columns if needed.
2077    ///
2078    /// # Arguments
2079    /// * `text` - The text of the bullet point (without the "* " prefix)
2080    ///
2081    /// # Examples
2082    /// ```
2083    /// use debian_changelog::ChangeLog;
2084    ///
2085    /// let mut changelog = ChangeLog::new();
2086    /// let entry = changelog.new_entry()
2087    ///     .maintainer(("Author".into(), "author@example.com".into()))
2088    ///     .distribution("unstable".to_string())
2089    ///     .version("1.0.0".parse().unwrap())
2090    ///     .finish();
2091    ///
2092    /// entry.add_bullet("First change");
2093    /// entry.add_bullet("Second change");
2094    ///
2095    /// let lines: Vec<_> = entry.change_lines().collect();
2096    /// assert_eq!(lines[0], "* First change");
2097    /// assert_eq!(lines[1], "* Second change");
2098    /// ```
2099    pub fn add_bullet(&self, text: &str) {
2100        // Wrap the text with "* " prefix
2101        let wrapped = crate::textwrap::textwrap(
2102            text,
2103            Some(crate::textwrap::DEFAULT_WIDTH),
2104            Some(crate::textwrap::INITIAL_INDENT),
2105            Some("  "),
2106        );
2107
2108        // Append each wrapped line
2109        for line in wrapped {
2110            self.append_change_line(&line);
2111        }
2112    }
2113
2114    /// Returns the changes of the entry.
2115    pub fn change_lines(&self) -> impl Iterator<Item = String> + '_ {
2116        let mut lines = self
2117            .0
2118            .children()
2119            .filter_map(|n| {
2120                if let Some(ref change) = EntryBody::cast(n.clone()) {
2121                    Some(change.text())
2122                } else if n.kind() == EMPTY_LINE {
2123                    Some("".to_string())
2124                } else {
2125                    None
2126                }
2127            })
2128            .collect::<Vec<_>>();
2129
2130        while let Some(last) = lines.last() {
2131            if last.is_empty() {
2132                lines.pop();
2133            } else {
2134                break;
2135            }
2136        }
2137
2138        lines.into_iter().skip_while(|it| it.is_empty())
2139    }
2140
2141    /// Ensure that the first line of the entry is the specified line
2142    ///
2143    /// If the first line is not the specified line, it will be prepended to the entry.
2144    pub fn ensure_first_line(&self, line: &str) {
2145        let first_line = self.change_lines().next().map(|it| it.trim().to_string());
2146
2147        if first_line != Some(line.to_string()) {
2148            self.prepend_change_line(line);
2149        }
2150    }
2151
2152    /// Return whether the entry is marked as being unreleased
2153    pub fn is_unreleased(&self) -> Option<bool> {
2154        let distro_is_unreleased = self.distributions().as_ref().map(|ds| {
2155            let ds = ds.iter().map(|d| d.as_str()).collect::<Vec<&str>>();
2156            crate::distributions_is_unreleased(ds.as_slice())
2157        });
2158
2159        let footer_is_unreleased = if self.maintainer().is_none() && self.email().is_none() {
2160            Some(true)
2161        } else {
2162            None
2163        };
2164
2165        match (distro_is_unreleased, footer_is_unreleased) {
2166            (Some(true), _) => Some(true),
2167            (_, Some(true)) => Some(true),
2168            (Some(false), _) => Some(false),
2169            (_, Some(false)) => Some(false),
2170            _ => None,
2171        }
2172    }
2173
2174    /// Iterator over changes in this entry grouped by author.
2175    ///
2176    /// Returns a vector of tuples (author_name, line_numbers, change_lines)
2177    /// where author_name is Some for attributed changes or None for changes without attribution.
2178    pub fn iter_changes_by_author(&self) -> Vec<(Option<String>, Vec<usize>, Vec<String>)> {
2179        let changes: Vec<String> = self.change_lines().map(|s| s.to_string()).collect();
2180        crate::changes::changes_by_author(changes.iter().map(|s| s.as_str()))
2181            .map(|(author, linenos, lines)| {
2182                let author_name = author.map(|s| s.to_string());
2183                let change_lines = lines.into_iter().map(|s| s.to_string()).collect();
2184                (author_name, linenos, change_lines)
2185            })
2186            .collect()
2187    }
2188
2189    /// Get all authors mentioned in this entry's changes.
2190    ///
2191    /// This includes authors from [ Author Name ] sections in the change text,
2192    /// but not the main maintainer/uploader from the entry footer.
2193    pub fn get_authors(&self) -> std::collections::HashSet<String> {
2194        let changes: Vec<String> = self.change_lines().map(|s| s.to_string()).collect();
2195        let change_strs: Vec<&str> = changes.iter().map(|s| s.as_str()).collect();
2196        crate::changes::find_extra_authors(&change_strs)
2197            .into_iter()
2198            .map(|s| s.to_string())
2199            .collect()
2200    }
2201
2202    /// Get the maintainer information as an Identity struct.
2203    ///
2204    /// Returns the maintainer name and email from the entry footer if available.
2205    pub fn get_maintainer_identity(&self) -> Option<crate::Identity> {
2206        if let (Some(name), Some(email)) = (self.maintainer(), self.email()) {
2207            Some(crate::Identity::new(name, email))
2208        } else {
2209            None
2210        }
2211    }
2212
2213    /// Add changes for a specific author to this entry.
2214    ///
2215    /// This will add an author section (e.g., `[ Author Name ]`) if needed,
2216    /// and append the changes under that section. If this is the first attributed
2217    /// change and there are existing unattributed changes, they will be wrapped
2218    /// in the maintainer's section.
2219    ///
2220    /// # Arguments
2221    /// * `author_name` - The name of the author to attribute the changes to
2222    /// * `changes` - A list of change lines to add (e.g., `["* Fixed bug"]`)
2223    ///
2224    /// # Example
2225    /// ```
2226    /// use debian_changelog::Entry;
2227    /// let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2228    ///
2229    ///   * Existing change
2230    ///
2231    ///  -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2232    /// "#.parse().unwrap();
2233    ///
2234    /// entry.try_add_changes_for_author("Alice", vec!["* New feature"]);
2235    /// ```
2236    pub fn try_add_changes_for_author(
2237        &self,
2238        author_name: &str,
2239        changes: Vec<&str>,
2240    ) -> Result<(), crate::textwrap::Error> {
2241        let mut change_lines: Vec<String> = self.change_lines().collect();
2242        let original_len = change_lines.len();
2243        let default_author = self.get_maintainer_identity().map(|id| (id.name, id.email));
2244
2245        crate::changes::try_add_change_for_author(
2246            &mut change_lines,
2247            author_name,
2248            changes,
2249            default_author,
2250        )?;
2251
2252        // The function modifies change_lines in place. We need to handle two cases:
2253        // 1. Lines were inserted at the beginning (when wrapping existing changes)
2254        // 2. Lines were appended at the end (normal case)
2255
2256        if change_lines.len() > original_len {
2257            // New lines were added
2258            let original_changes: Vec<_> = self.change_lines().collect();
2259
2260            // Check if lines were inserted at the start
2261            let inserted_at_start = original_len > 0 && change_lines[0] != original_changes[0];
2262
2263            if inserted_at_start {
2264                // Lines were inserted at the beginning - we need to rebuild
2265                // This happens when converting unattributed changes to attributed ones
2266                while self.pop_change_line().is_some() {}
2267                for line in change_lines {
2268                    self.append_change_line(&line);
2269                }
2270            } else {
2271                // Lines were appended at the end - just append the new ones
2272                for line in change_lines.iter().skip(original_len) {
2273                    self.append_change_line(line);
2274                }
2275            }
2276        }
2277        Ok(())
2278    }
2279
2280    /// Add changes for the specified author
2281    ///
2282    /// # Deprecated
2283    ///
2284    /// This function panics on errors. Use [`Entry::try_add_changes_for_author`] instead for proper error handling.
2285    ///
2286    /// # Panics
2287    ///
2288    /// Panics if text rewrapping fails.
2289    #[deprecated(
2290        since = "0.2.10",
2291        note = "Use try_add_changes_for_author for proper error handling"
2292    )]
2293    pub fn add_changes_for_author(&self, author_name: &str, changes: Vec<&str>) {
2294        self.try_add_changes_for_author(author_name, changes)
2295            .unwrap()
2296    }
2297}
2298
2299#[cfg(feature = "chrono")]
2300const CHANGELOG_TIME_FORMAT: &str = "%a, %d %b %Y %H:%M:%S %z";
2301
2302#[cfg(feature = "chrono")]
2303fn parse_time_string(time_str: &str) -> Result<DateTime<FixedOffset>, chrono::ParseError> {
2304    // First try parsing with day-of-week validation
2305    if let Ok(dt) = DateTime::parse_from_str(time_str, CHANGELOG_TIME_FORMAT) {
2306        return Ok(dt);
2307    }
2308
2309    // If that fails, try parsing without day-of-week validation
2310    // This is more lenient for changelogs that have incorrect day-of-week values
2311    // Skip the day name (everything before the first comma and space)
2312    if let Some(after_comma) = time_str.split_once(", ") {
2313        DateTime::parse_from_str(after_comma.1, "%d %b %Y %H:%M:%S %z")
2314    } else {
2315        // If there's no comma, return the original error
2316        DateTime::parse_from_str(time_str, CHANGELOG_TIME_FORMAT)
2317    }
2318}
2319
2320#[cfg(test)]
2321mod tests {
2322    use super::*;
2323
2324    #[test]
2325    fn test_parse_simple() {
2326        const CHANGELOG: &str = r#"breezy (3.3.4-1) unstable; urgency=low
2327
2328  * New upstream release.
2329
2330 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2331
2332breezy (3.3.3-2) unstable; urgency=medium
2333
2334  * Drop unnecessary dependency on python3-six. Closes: #1039011
2335  * Drop dependency on cython3-dbg. Closes: #1040544
2336
2337 -- Jelmer Vernooij <jelmer@debian.org>  Sat, 24 Jun 2023 14:58:57 +0100
2338
2339# Oh, and here is a comment
2340"#;
2341        let parsed = parse(CHANGELOG);
2342        assert_eq!(parsed.errors(), &Vec::<String>::new());
2343        let node = parsed.syntax_node();
2344        assert_eq!(
2345            format!("{:#?}", node),
2346            r###"ROOT@0..405
2347  ENTRY@0..140
2348    ENTRY_HEADER@0..39
2349      IDENTIFIER@0..6 "breezy"
2350      WHITESPACE@6..7 " "
2351      VERSION@7..16 "(3.3.4-1)"
2352      WHITESPACE@16..17 " "
2353      DISTRIBUTIONS@17..25
2354        IDENTIFIER@17..25 "unstable"
2355      METADATA@25..38
2356        SEMICOLON@25..26 ";"
2357        WHITESPACE@26..27 " "
2358        METADATA_ENTRY@27..38
2359          METADATA_KEY@27..34
2360            IDENTIFIER@27..34 "urgency"
2361          EQUALS@34..35 "="
2362          METADATA_VALUE@35..38
2363            IDENTIFIER@35..38 "low"
2364      NEWLINE@38..39 "\n"
2365    EMPTY_LINE@39..40
2366      NEWLINE@39..40 "\n"
2367    ENTRY_BODY@40..66
2368      INDENT@40..42 "  "
2369      DETAIL@42..65 "* New upstream release."
2370      NEWLINE@65..66 "\n"
2371    EMPTY_LINE@66..67
2372      NEWLINE@66..67 "\n"
2373    ENTRY_FOOTER@67..140
2374      INDENT@67..71 " -- "
2375      MAINTAINER@71..86
2376        TEXT@71..77 "Jelmer"
2377        WHITESPACE@77..78 " "
2378        TEXT@78..86 "Vernooij"
2379      WHITESPACE@86..87 " "
2380      EMAIL@87..106 "<jelmer@debian.org>"
2381      WHITESPACE@106..108 "  "
2382      TIMESTAMP@108..139
2383        TEXT@108..112 "Mon,"
2384        WHITESPACE@112..113 " "
2385        TEXT@113..115 "04"
2386        WHITESPACE@115..116 " "
2387        TEXT@116..119 "Sep"
2388        WHITESPACE@119..120 " "
2389        TEXT@120..124 "2023"
2390        WHITESPACE@124..125 " "
2391        TEXT@125..133 "18:13:45"
2392        WHITESPACE@133..134 " "
2393        TEXT@134..139 "-0500"
2394      NEWLINE@139..140 "\n"
2395  EMPTY_LINE@140..141
2396    NEWLINE@140..141 "\n"
2397  ENTRY@141..376
2398    ENTRY_HEADER@141..183
2399      IDENTIFIER@141..147 "breezy"
2400      WHITESPACE@147..148 " "
2401      VERSION@148..157 "(3.3.3-2)"
2402      WHITESPACE@157..158 " "
2403      DISTRIBUTIONS@158..166
2404        IDENTIFIER@158..166 "unstable"
2405      METADATA@166..182
2406        SEMICOLON@166..167 ";"
2407        WHITESPACE@167..168 " "
2408        METADATA_ENTRY@168..182
2409          METADATA_KEY@168..175
2410            IDENTIFIER@168..175 "urgency"
2411          EQUALS@175..176 "="
2412          METADATA_VALUE@176..182
2413            IDENTIFIER@176..182 "medium"
2414      NEWLINE@182..183 "\n"
2415    EMPTY_LINE@183..184
2416      NEWLINE@183..184 "\n"
2417    ENTRY_BODY@184..249
2418      INDENT@184..186 "  "
2419      DETAIL@186..248 "* Drop unnecessary de ..."
2420      NEWLINE@248..249 "\n"
2421    ENTRY_BODY@249..302
2422      INDENT@249..251 "  "
2423      DETAIL@251..301 "* Drop dependency on  ..."
2424      NEWLINE@301..302 "\n"
2425    EMPTY_LINE@302..303
2426      NEWLINE@302..303 "\n"
2427    ENTRY_FOOTER@303..376
2428      INDENT@303..307 " -- "
2429      MAINTAINER@307..322
2430        TEXT@307..313 "Jelmer"
2431        WHITESPACE@313..314 " "
2432        TEXT@314..322 "Vernooij"
2433      WHITESPACE@322..323 " "
2434      EMAIL@323..342 "<jelmer@debian.org>"
2435      WHITESPACE@342..344 "  "
2436      TIMESTAMP@344..375
2437        TEXT@344..348 "Sat,"
2438        WHITESPACE@348..349 " "
2439        TEXT@349..351 "24"
2440        WHITESPACE@351..352 " "
2441        TEXT@352..355 "Jun"
2442        WHITESPACE@355..356 " "
2443        TEXT@356..360 "2023"
2444        WHITESPACE@360..361 " "
2445        TEXT@361..369 "14:58:57"
2446        WHITESPACE@369..370 " "
2447        TEXT@370..375 "+0100"
2448      NEWLINE@375..376 "\n"
2449  EMPTY_LINE@376..377
2450    NEWLINE@376..377 "\n"
2451  COMMENT@377..405 "# Oh, and here is a c ..."
2452"###
2453        );
2454
2455        let mut root = parsed.tree_mut();
2456        let entries: Vec<_> = root.iter().collect();
2457        assert_eq!(entries.len(), 2);
2458        let entry = &entries[0];
2459        assert_eq!(entry.package(), Some("breezy".into()));
2460        assert_eq!(entry.version(), Some("3.3.4-1".parse().unwrap()));
2461        assert_eq!(entry.distributions(), Some(vec!["unstable".into()]));
2462        assert_eq!(entry.urgency(), Some(Urgency::Low));
2463        assert_eq!(entry.maintainer(), Some("Jelmer Vernooij".into()));
2464        assert_eq!(entry.email(), Some("jelmer@debian.org".into()));
2465        assert_eq!(
2466            entry.timestamp(),
2467            Some("Mon, 04 Sep 2023 18:13:45 -0500".into())
2468        );
2469        #[cfg(feature = "chrono")]
2470        assert_eq!(
2471            entry.datetime(),
2472            Some("2023-09-04T18:13:45-05:00".parse().unwrap())
2473        );
2474        let changes_lines: Vec<_> = entry.change_lines().collect();
2475        assert_eq!(changes_lines, vec!["* New upstream release.".to_string()]);
2476
2477        assert_eq!(node.text(), CHANGELOG);
2478
2479        let first = root.pop_first().unwrap();
2480        assert_eq!(first.version(), Some("3.3.4-1".parse().unwrap()));
2481        assert_eq!(
2482            root.to_string(),
2483            r#"breezy (3.3.3-2) unstable; urgency=medium
2484
2485  * Drop unnecessary dependency on python3-six. Closes: #1039011
2486  * Drop dependency on cython3-dbg. Closes: #1040544
2487
2488 -- Jelmer Vernooij <jelmer@debian.org>  Sat, 24 Jun 2023 14:58:57 +0100
2489
2490# Oh, and here is a comment
2491"#
2492        );
2493    }
2494
2495    #[test]
2496    fn test_from_io_read() {
2497        let changelog = r#"breezy (3.3.4-1) unstable; urgency=low
2498
2499  * New upstream release.
2500
2501 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2502"#;
2503
2504        let input = changelog.as_bytes();
2505        let input = Box::new(std::io::Cursor::new(input)) as Box<dyn std::io::Read>;
2506        let parsed = ChangeLog::read(input).unwrap();
2507        assert_eq!(parsed.to_string(), changelog);
2508    }
2509
2510    #[test]
2511    #[cfg(feature = "chrono")]
2512    fn test_new_entry() {
2513        let mut cl = ChangeLog::new();
2514        cl.new_entry()
2515            .package("breezy".into())
2516            .version("3.3.4-1".parse().unwrap())
2517            .distributions(vec!["unstable".into()])
2518            .urgency(Urgency::Low)
2519            .maintainer(("Jelmer Vernooij".into(), "jelmer@debian.org".into()))
2520            .change_line("* A change.".into())
2521            .datetime("Mon, 04 Sep 2023 18:13:45 -0500")
2522            .finish();
2523        assert_eq!(
2524            r###"breezy (3.3.4-1) unstable; urgency=low
2525
2526  * A change.
2527
2528 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2529"###,
2530            cl.to_string()
2531        );
2532
2533        assert!(!cl.iter().next().unwrap().is_unreleased().unwrap());
2534    }
2535
2536    #[test]
2537    #[cfg(feature = "chrono")]
2538    fn test_new_empty_default() {
2539        let mut cl = ChangeLog::new();
2540        cl.new_entry()
2541            .package("breezy".into())
2542            .version("3.3.4-1".parse().unwrap())
2543            .maintainer(("Jelmer Vernooij".into(), "jelmer@debian.org".into()))
2544            .change_line("* A change.".into())
2545            .datetime("Mon, 04 Sep 2023 18:13:45 -0500")
2546            .finish();
2547        assert_eq!(
2548            r###"breezy (3.3.4-1) UNRELEASED; urgency=low
2549
2550  * A change.
2551
2552 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2553"###,
2554            cl.to_string()
2555        );
2556    }
2557
2558    #[test]
2559    fn test_new_empty_entry() {
2560        let mut cl = ChangeLog::new();
2561        cl.new_empty_entry()
2562            .change_line("* A change.".into())
2563            .finish();
2564        assert_eq!(
2565            r###"
2566
2567  * A change.
2568
2569 -- 
2570"###,
2571            cl.to_string()
2572        );
2573        assert_eq!(cl.iter().next().unwrap().is_unreleased(), Some(true));
2574    }
2575
2576    #[test]
2577    fn test_parse_invalid_line() {
2578        let text = r#"THIS IS NOT A PARSEABLE LINE
2579lintian-brush (0.35) UNRELEASED; urgency=medium
2580
2581  * Support updating templated debian/control files that use cdbs
2582    template.
2583
2584 -- Joe Example <joe@example.com>  Fri, 04 Oct 2019 02:36:13 +0000
2585"#;
2586        let cl = ChangeLog::read_relaxed(text.as_bytes()).unwrap();
2587        let entry = cl.iter().nth(1).unwrap();
2588        assert_eq!(entry.package(), Some("lintian-brush".into()));
2589        assert_eq!(entry.version(), Some("0.35".parse().unwrap()));
2590        assert_eq!(entry.urgency(), Some(Urgency::Medium));
2591        assert_eq!(entry.maintainer(), Some("Joe Example".into()));
2592        assert_eq!(entry.email(), Some("joe@example.com".into()));
2593        assert_eq!(entry.distributions(), Some(vec!["UNRELEASED".into()]));
2594        #[cfg(feature = "chrono")]
2595        assert_eq!(
2596            entry.datetime(),
2597            Some("2019-10-04T02:36:13+00:00".parse().unwrap())
2598        );
2599    }
2600
2601    #[cfg(test)]
2602    mod entry_manipulate_tests {
2603        use super::*;
2604
2605        #[test]
2606        fn test_append_change_line() {
2607            let mut cl = ChangeLog::new();
2608
2609            let entry = cl
2610                .new_empty_entry()
2611                .change_line("* A change.".into())
2612                .finish();
2613
2614            entry.append_change_line("* Another change.");
2615
2616            assert_eq!(
2617                r###"
2618
2619  * A change.
2620  * Another change.
2621
2622 -- 
2623"###,
2624                cl.to_string()
2625            );
2626        }
2627
2628        #[test]
2629        fn test_prepend_change_line() {
2630            let mut cl = ChangeLog::new();
2631
2632            let entry = cl
2633                .new_empty_entry()
2634                .change_line("* A change.".into())
2635                .finish();
2636
2637            entry.prepend_change_line("* Another change.");
2638
2639            assert_eq!(
2640                r###"
2641
2642  * Another change.
2643  * A change.
2644
2645 -- 
2646"###,
2647                cl.to_string()
2648            );
2649
2650            assert_eq!(entry.maintainer(), None);
2651            assert_eq!(entry.email(), None);
2652            assert_eq!(entry.timestamp(), None);
2653            assert_eq!(entry.package(), None);
2654            assert_eq!(entry.version(), None);
2655        }
2656    }
2657
2658    #[cfg(test)]
2659    mod auto_add_change_tests {
2660        #[test]
2661        fn test_unreleased_existing() {
2662            let text = r#"lintian-brush (0.35) unstable; urgency=medium
2663
2664  * This line already existed.
2665
2666  [ Jane Example ]
2667  * And this one has an existing author.
2668
2669 -- 
2670"#;
2671            let mut cl = super::ChangeLog::read(text.as_bytes()).unwrap();
2672
2673            let entry = cl.iter().next().unwrap();
2674            assert_eq!(entry.package(), Some("lintian-brush".into()));
2675            assert_eq!(entry.is_unreleased(), Some(true));
2676
2677            let entry = cl
2678                .try_auto_add_change(
2679                    &["* And this one is new."],
2680                    ("Joe Example".to_string(), "joe@example.com".to_string()),
2681                    None::<String>,
2682                    None,
2683                )
2684                .unwrap();
2685
2686            assert_eq!(cl.iter().count(), 1);
2687
2688            assert_eq!(entry.package(), Some("lintian-brush".into()));
2689            assert_eq!(entry.is_unreleased(), Some(true));
2690            assert_eq!(
2691                entry.change_lines().collect::<Vec<_>>(),
2692                &[
2693                    "* This line already existed.",
2694                    "",
2695                    "[ Jane Example ]",
2696                    "* And this one has an existing author.",
2697                    "",
2698                    "[ Joe Example ]",
2699                    "* And this one is new.",
2700                ]
2701            );
2702        }
2703    }
2704
2705    #[test]
2706    fn test_ensure_first_line() {
2707        let text = r#"lintian-brush (0.35) unstable; urgency=medium
2708
2709  * This line already existed.
2710
2711  [ Jane Example ]
2712  * And this one has an existing author.
2713
2714 -- 
2715"#;
2716        let cl = ChangeLog::read(text.as_bytes()).unwrap();
2717
2718        let entry = cl.iter().next().unwrap();
2719        assert_eq!(entry.package(), Some("lintian-brush".into()));
2720
2721        entry.ensure_first_line("* QA upload.");
2722        entry.ensure_first_line("* QA upload.");
2723
2724        assert_eq!(
2725            r#"lintian-brush (0.35) unstable; urgency=medium
2726
2727  * QA upload.
2728  * This line already existed.
2729
2730  [ Jane Example ]
2731  * And this one has an existing author.
2732
2733 -- 
2734"#,
2735            cl.to_string()
2736        );
2737    }
2738
2739    #[test]
2740    fn test_set_version() {
2741        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2742
2743  * New upstream release.
2744
2745 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2746"#
2747        .parse()
2748        .unwrap();
2749
2750        entry.set_version(&"3.3.5-1".parse().unwrap());
2751
2752        assert_eq!(
2753            r#"breezy (3.3.5-1) unstable; urgency=low
2754
2755  * New upstream release.
2756
2757 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2758"#,
2759            entry.to_string()
2760        );
2761    }
2762
2763    #[test]
2764    fn test_set_package() {
2765        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2766
2767  * New upstream release.
2768
2769 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2770"#
2771        .parse()
2772        .unwrap();
2773
2774        entry.set_package("bzr".into());
2775
2776        assert_eq!(
2777            r#"bzr (3.3.4-1) unstable; urgency=low
2778
2779  * New upstream release.
2780
2781 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2782"#,
2783            entry.to_string()
2784        );
2785    }
2786
2787    #[test]
2788    fn test_set_distributions() {
2789        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2790
2791  * New upstream release.
2792
2793 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2794"#
2795        .parse()
2796        .unwrap();
2797
2798        entry.set_distributions(vec!["unstable".into(), "experimental".into()]);
2799
2800        assert_eq!(
2801            r#"breezy (3.3.4-1) unstable experimental; urgency=low
2802
2803  * New upstream release.
2804
2805 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2806"#,
2807            entry.to_string()
2808        );
2809    }
2810
2811    #[test]
2812    fn test_set_distributions_no_existing() {
2813        let mut entry: Entry = r#"breezy (3.3.4-1); urgency=low
2814
2815  * New upstream release.
2816
2817 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2818"#
2819        .parse()
2820        .unwrap();
2821
2822        entry.set_distributions(vec!["unstable".into()]);
2823
2824        assert!(entry.to_string().contains("unstable"));
2825    }
2826
2827    #[test]
2828    fn test_set_maintainer() {
2829        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2830
2831  * New upstream release.
2832
2833 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2834"#
2835        .parse()
2836        .unwrap();
2837
2838        entry.set_maintainer(("Joe Example".into(), "joe@example.com".into()));
2839
2840        assert_eq!(
2841            r#"breezy (3.3.4-1) unstable; urgency=low
2842
2843  * New upstream release.
2844
2845 -- Joe Example <joe@example.com>  Mon, 04 Sep 2023 18:13:45 -0500
2846"#,
2847            entry.to_string()
2848        );
2849    }
2850
2851    #[test]
2852    fn test_set_timestamp() {
2853        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2854
2855  * New upstream release.
2856
2857 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2858"#
2859        .parse()
2860        .unwrap();
2861
2862        entry.set_timestamp("Mon, 04 Sep 2023 18:13:46 -0500".into());
2863
2864        assert_eq!(
2865            r#"breezy (3.3.4-1) unstable; urgency=low
2866
2867  * New upstream release.
2868
2869 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:46 -0500
2870"#,
2871            entry.to_string()
2872        );
2873    }
2874
2875    #[test]
2876    #[cfg(feature = "chrono")]
2877    fn test_set_datetime() {
2878        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2879
2880  * New upstream release.
2881
2882 -- Jelmer Vernooij <joe@example.com>  Mon, 04 Sep 2023 18:13:45 -0500
2883"#
2884        .parse()
2885        .unwrap();
2886
2887        entry.set_datetime("2023-09-04T18:13:46-05:00".parse().unwrap());
2888
2889        assert_eq!(
2890            r#"breezy (3.3.4-1) unstable; urgency=low
2891
2892  * New upstream release.
2893
2894 -- Jelmer Vernooij <joe@example.com>  Mon, 04 Sep 2023 18:13:46 -0500
2895"#,
2896            entry.to_string()
2897        );
2898    }
2899
2900    #[test]
2901    fn test_set_urgency() {
2902        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2903
2904  * New upstream release.
2905
2906 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2907"#
2908        .parse()
2909        .unwrap();
2910
2911        entry.set_urgency(Urgency::Medium);
2912
2913        assert_eq!(
2914            r#"breezy (3.3.4-1) unstable; urgency=medium
2915
2916  * New upstream release.
2917
2918 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2919"#,
2920            entry.to_string()
2921        );
2922    }
2923
2924    #[test]
2925    fn test_set_metadata() {
2926        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2927
2928  * New upstream release.
2929
2930 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2931"#
2932        .parse()
2933        .unwrap();
2934
2935        entry.set_metadata("foo", "bar");
2936
2937        assert_eq!(
2938            r#"breezy (3.3.4-1) unstable; urgency=low foo=bar
2939
2940  * New upstream release.
2941
2942 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2943"#,
2944            entry.to_string()
2945        );
2946    }
2947
2948    #[test]
2949    fn test_set_metadata_replace_existing() {
2950        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low foo=old
2951
2952  * New upstream release.
2953
2954 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2955"#
2956        .parse()
2957        .unwrap();
2958
2959        entry.set_metadata("foo", "new");
2960
2961        assert_eq!(
2962            r#"breezy (3.3.4-1) unstable; urgency=low foo=new
2963
2964  * New upstream release.
2965
2966 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2967"#,
2968            entry.to_string()
2969        );
2970    }
2971
2972    #[test]
2973    fn test_set_metadata_after_distributions() {
2974        let mut entry: Entry = r#"breezy (3.3.4-1) unstable experimental; urgency=low
2975
2976  * New upstream release.
2977
2978 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2979"#
2980        .parse()
2981        .unwrap();
2982
2983        entry.set_metadata("foo", "bar");
2984
2985        assert_eq!(
2986            r#"breezy (3.3.4-1) unstable experimental; urgency=low foo=bar
2987
2988  * New upstream release.
2989
2990 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2991"#,
2992            entry.to_string()
2993        );
2994    }
2995
2996    #[test]
2997    fn test_add_change_for_author() {
2998        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2999
3000  * New upstream release.
3001
3002  [ Jelmer Vernooij ]
3003  * A change by the maintainer.
3004
3005 -- Joe Example <joe@example.com>  Mon, 04 Sep 2023 18:13:45 -0500
3006"#
3007        .parse()
3008        .unwrap();
3009
3010        entry
3011            .try_add_change_for_author(
3012                &["A change by the maintainer."],
3013                ("Jelmer Vernooij".into(), "jelmer@debian.org".into()),
3014            )
3015            .unwrap();
3016    }
3017
3018    #[test]
3019    fn test_changelog_from_entry_iter() {
3020        let text = r#"breezy (3.3.4-1) unstable; urgency=low
3021
3022  * New upstream release.
3023
3024 -- Jelmer Vernooij <jelmer@jelmer.uk>  Mon, 04 Sep 2023 18:13:45 -0500
3025"#;
3026
3027        let entry: Entry = text.parse().unwrap();
3028
3029        let cl = std::iter::once(entry).collect::<ChangeLog>();
3030
3031        assert_eq!(cl.to_string(), text);
3032    }
3033
3034    #[test]
3035    fn test_pop_change_line() {
3036        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3037
3038  * New upstream release.
3039  * Fixed bug #123.
3040  * Added new feature.
3041
3042 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3043"#
3044        .parse()
3045        .unwrap();
3046
3047        // Test popping existing lines
3048        assert_eq!(
3049            entry.pop_change_line(),
3050            Some("* Added new feature.".to_string())
3051        );
3052        assert_eq!(
3053            entry.pop_change_line(),
3054            Some("* Fixed bug #123.".to_string())
3055        );
3056        assert_eq!(
3057            entry.pop_change_line(),
3058            Some("* New upstream release.".to_string())
3059        );
3060
3061        // Test popping from empty entry
3062        assert_eq!(entry.pop_change_line(), None);
3063    }
3064
3065    #[test]
3066    fn test_pop_change_line_empty_entry() {
3067        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3068
3069 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3070"#
3071        .parse()
3072        .unwrap();
3073
3074        assert_eq!(entry.pop_change_line(), None);
3075    }
3076
3077    #[test]
3078    fn test_pop_change_line_empty_string() {
3079        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3080
3081  * Something
3082
3083 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3084"#
3085        .parse()
3086        .unwrap();
3087
3088        entry.pop_change_line();
3089        entry.append_change_line("");
3090        // Empty lines don't have DETAIL tokens, so pop_change_line returns None
3091        assert_eq!(entry.pop_change_line(), None);
3092    }
3093
3094    #[test]
3095    fn test_append_change_line() {
3096        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3097
3098  * New upstream release.
3099
3100 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3101"#
3102        .parse()
3103        .unwrap();
3104
3105        entry.append_change_line("* Fixed bug #456.");
3106
3107        assert_eq!(
3108            entry.to_string(),
3109            r#"breezy (3.3.4-1) unstable; urgency=low
3110
3111  * New upstream release.
3112  * Fixed bug #456.
3113
3114 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3115"#
3116        );
3117    }
3118
3119    #[test]
3120    fn test_append_change_line_empty() {
3121        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3122
3123  * New upstream release.
3124
3125 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3126"#
3127        .parse()
3128        .unwrap();
3129
3130        entry.append_change_line("");
3131
3132        let lines: Vec<String> = entry.change_lines().collect();
3133        // Empty lines are not returned by change_lines()
3134        assert_eq!(lines.len(), 1);
3135        assert_eq!(lines[0], "* New upstream release.".to_string());
3136    }
3137
3138    #[test]
3139    fn test_changelog_write_to_path() {
3140        use tempfile::NamedTempFile;
3141
3142        let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
3143
3144  * New upstream release.
3145
3146 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3147"#
3148        .parse()
3149        .unwrap();
3150
3151        let temp_file = NamedTempFile::new().unwrap();
3152        let path = temp_file.path().to_path_buf();
3153
3154        changelog.write_to_path(&path).unwrap();
3155
3156        let contents = std::fs::read_to_string(&path).unwrap();
3157        assert_eq!(contents, changelog.to_string());
3158    }
3159
3160    #[test]
3161    fn test_changelog_into_iter() {
3162        let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
3163
3164  * New upstream release.
3165
3166 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3167
3168breezy (3.3.3-1) unstable; urgency=low
3169
3170  * Previous release.
3171
3172 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 03 Sep 2023 18:13:45 -0500
3173"#
3174        .parse()
3175        .unwrap();
3176
3177        let entries: Vec<Entry> = changelog.into_iter().collect();
3178        assert_eq!(entries.len(), 2);
3179    }
3180
3181    #[test]
3182    fn test_set_version_no_existing() {
3183        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3184
3185  * New upstream release.
3186
3187 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3188"#
3189        .parse()
3190        .unwrap();
3191
3192        entry.set_version(&"1.0.0".parse().unwrap());
3193
3194        assert!(entry.to_string().contains("(1.0.0)"));
3195    }
3196
3197    #[test]
3198    fn test_entry_footer_set_email_edge_cases() {
3199        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3200
3201  * New upstream release.
3202
3203 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3204"#
3205        .parse()
3206        .unwrap();
3207
3208        // Test checking email through entry
3209        assert_eq!(entry.email(), Some("jelmer@debian.org".to_string()));
3210    }
3211
3212    #[test]
3213    fn test_entry_footer_set_maintainer_edge_cases() {
3214        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3215
3216  * New upstream release.
3217
3218 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3219"#
3220        .parse()
3221        .unwrap();
3222
3223        // Test setting maintainer
3224        entry.set_maintainer(("New Maintainer".into(), "new@example.com".into()));
3225
3226        assert!(entry
3227            .to_string()
3228            .contains("New Maintainer <new@example.com>"));
3229    }
3230
3231    #[test]
3232    fn test_entry_footer_set_timestamp_edge_cases() {
3233        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3234
3235  * New upstream release.
3236
3237 -- Jelmer Vernooij <jelmer@debian.org>  
3238"#
3239        .parse()
3240        .unwrap();
3241
3242        // Test setting timestamp when it's missing
3243        entry.set_timestamp("Mon, 04 Sep 2023 18:13:45 -0500".into());
3244
3245        assert!(entry
3246            .to_string()
3247            .contains("Mon, 04 Sep 2023 18:13:45 -0500"));
3248    }
3249
3250    #[test]
3251    fn test_parse_multiple_distributions_frozen_unstable() {
3252        // Test case for https://github.com/jelmer/debian-changelog-rs/issues/93
3253        // The "at" package has entries with "frozen unstable" distributions from 1998
3254        const CHANGELOG: &str = r#"at (3.1.8-10) frozen unstable; urgency=high
3255
3256  * Suidunregister /usr/bin (closes: Bug#59421).
3257
3258 -- Siggy Brentrup <bsb@winnegan.de>  Mon,  3 Apr 2000 13:56:47 +0200
3259"#;
3260
3261        let parsed = parse(CHANGELOG);
3262        assert_eq!(parsed.errors(), &Vec::<String>::new());
3263
3264        let root = parsed.tree();
3265        let entries: Vec<_> = root.iter().collect();
3266        assert_eq!(entries.len(), 1);
3267
3268        let entry = &entries[0];
3269        assert_eq!(entry.package(), Some("at".into()));
3270        assert_eq!(entry.version(), Some("3.1.8-10".parse().unwrap()));
3271        assert_eq!(
3272            entry.distributions(),
3273            Some(vec!["frozen".into(), "unstable".into()])
3274        );
3275    }
3276
3277    #[test]
3278    fn test_parse_old_metadata_format_with_comma() {
3279        // Test case for https://github.com/jelmer/debian-changelog-rs/issues/93
3280        // The "at" package has old-style metadata with comma-separated values
3281        const CHANGELOG: &str = r#"at (3.1.8-9) frozen unstable; urgency=low, closes=53715 56047 56607 55560 55514
3282
3283  * Added SIGCHLD handler to release zombies (closes 53715 56047 56607)
3284
3285 -- Siggy Brentrup <bsb@winnegan.de>  Sun, 30 Jan 2000 22:00:46 +0100
3286"#;
3287
3288        let parsed = parse(CHANGELOG);
3289
3290        // This old format currently fails to parse
3291        if !parsed.errors().is_empty() {
3292            eprintln!("Parse errors: {:?}", parsed.errors());
3293        }
3294        assert_eq!(parsed.errors(), &Vec::<String>::new());
3295
3296        let root = parsed.tree();
3297        let entries: Vec<_> = root.iter().collect();
3298        assert_eq!(entries.len(), 1);
3299
3300        let entry = &entries[0];
3301        assert_eq!(entry.package(), Some("at".into()));
3302        assert_eq!(entry.version(), Some("3.1.8-9".parse().unwrap()));
3303        assert_eq!(
3304            entry.distributions(),
3305            Some(vec!["frozen".into(), "unstable".into()])
3306        );
3307        assert_eq!(entry.urgency(), Some(Urgency::Low));
3308
3309        // Verify we can access the "closes" metadata
3310        let header = entry.header().unwrap();
3311        let metadata: Vec<(String, String)> = header.metadata().collect();
3312
3313        // Should have both urgency and closes
3314        assert_eq!(metadata.len(), 2);
3315        assert!(metadata.iter().any(|(k, v)| k == "urgency" && v == "low"));
3316
3317        // Get the closes value and verify exact match
3318        let closes_value = metadata
3319            .iter()
3320            .find(|(k, _)| k == "closes")
3321            .map(|(_, v)| v)
3322            .expect("closes metadata should exist");
3323
3324        assert_eq!(closes_value, "53715 56047 56607 55560 55514");
3325    }
3326
3327    #[test]
3328    fn test_entry_iter_changes_by_author() {
3329        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3330
3331  [ Author 1 ]
3332  * Change by Author 1
3333
3334  [ Author 2 ]  
3335  * Change by Author 2
3336  * Another change by Author 2
3337
3338  * Unattributed change
3339
3340 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3341"#
3342        .parse()
3343        .unwrap();
3344
3345        let changes = entry.iter_changes_by_author();
3346
3347        assert_eq!(changes.len(), 3);
3348
3349        assert_eq!(changes[0].0, Some("Author 1".to_string()));
3350        assert_eq!(changes[0].2, vec!["* Change by Author 1".to_string()]);
3351
3352        assert_eq!(changes[1].0, Some("Author 2".to_string()));
3353        assert_eq!(
3354            changes[1].2,
3355            vec![
3356                "* Change by Author 2".to_string(),
3357                "* Another change by Author 2".to_string()
3358            ]
3359        );
3360
3361        assert_eq!(changes[2].0, None);
3362        assert_eq!(changes[2].2, vec!["* Unattributed change".to_string()]);
3363    }
3364
3365    #[test]
3366    fn test_entry_get_authors() {
3367        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3368
3369  [ Author 1 ]
3370  * Change by Author 1
3371
3372  [ Author 2 ]  
3373  * Change by Author 2
3374
3375 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3376"#
3377        .parse()
3378        .unwrap();
3379
3380        let authors = entry.get_authors();
3381
3382        assert_eq!(authors.len(), 2);
3383        assert!(authors.contains("Author 1"));
3384        assert!(authors.contains("Author 2"));
3385        // Maintainer should not be in the authors from change sections
3386        assert!(!authors.contains("Jelmer Vernooij"));
3387    }
3388
3389    #[test]
3390    fn test_entry_get_maintainer_identity() {
3391        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3392
3393  * New upstream release.
3394
3395 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3396"#
3397        .parse()
3398        .unwrap();
3399
3400        let identity = entry.get_maintainer_identity().unwrap();
3401
3402        assert_eq!(identity.name, "Jelmer Vernooij");
3403        assert_eq!(identity.email, "jelmer@debian.org");
3404    }
3405
3406    #[test]
3407    fn test_entry_get_maintainer_identity_missing() {
3408        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3409
3410  * New upstream release.
3411
3412"#
3413        .parse()
3414        .unwrap();
3415
3416        let identity = entry.get_maintainer_identity();
3417
3418        assert!(identity.is_none());
3419    }
3420
3421    #[test]
3422    fn test_changelog_iter_by_author() {
3423        let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
3424
3425  * New upstream release.
3426
3427 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3428
3429breezy (3.3.3-1) unstable; urgency=low
3430
3431  * Bug fix release.
3432
3433 -- Jane Doe <jane@example.com>  Sun, 03 Sep 2023 17:12:30 -0500
3434
3435breezy (3.3.2-1) unstable; urgency=low
3436
3437  * Another release.
3438
3439 -- Jelmer Vernooij <jelmer@debian.org>  Sat, 02 Sep 2023 16:11:15 -0500
3440"#
3441        .parse()
3442        .unwrap();
3443
3444        let authors: Vec<(String, String, Vec<Entry>)> = changelog.iter_by_author().collect();
3445
3446        assert_eq!(authors.len(), 2);
3447        assert_eq!(authors[0].0, "Jane Doe");
3448        assert_eq!(authors[0].1, "jane@example.com");
3449        assert_eq!(authors[0].2.len(), 1);
3450        assert_eq!(authors[1].0, "Jelmer Vernooij");
3451        assert_eq!(authors[1].1, "jelmer@debian.org");
3452        assert_eq!(authors[1].2.len(), 2);
3453    }
3454
3455    #[test]
3456    fn test_changelog_get_all_authors() {
3457        let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
3458
3459  [ Contributor 1 ]
3460  * Contribution
3461
3462  * Main change
3463
3464 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3465
3466breezy (3.3.3-1) unstable; urgency=low
3467
3468  * Bug fix release.
3469
3470 -- Jane Doe <jane@example.com>  Sun, 03 Sep 2023 17:12:30 -0500
3471"#
3472        .parse()
3473        .unwrap();
3474
3475        let authors = changelog.get_all_authors();
3476
3477        assert_eq!(authors.len(), 3);
3478
3479        let author_names: std::collections::HashSet<String> = authors
3480            .iter()
3481            .map(|identity| identity.name.clone())
3482            .collect();
3483
3484        assert!(author_names.contains("Jelmer Vernooij"));
3485        assert!(author_names.contains("Jane Doe"));
3486        assert!(author_names.contains("Contributor 1"));
3487    }
3488
3489    #[test]
3490    fn test_add_changes_for_author_no_existing_sections() {
3491        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3492
3493  * Existing change
3494
3495 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3496"#
3497        .parse()
3498        .unwrap();
3499
3500        entry
3501            .try_add_changes_for_author("Alice", vec!["* Alice's change"])
3502            .unwrap();
3503
3504        let lines: Vec<_> = entry.change_lines().collect();
3505
3506        // Should have wrapped existing changes in maintainer's section
3507        assert!(lines.iter().any(|l| l.contains("[ Jelmer Vernooij ]")));
3508        // Should have added Alice's section
3509        assert!(lines.iter().any(|l| l.contains("[ Alice ]")));
3510        // Should have both changes
3511        assert!(lines.iter().any(|l| l.contains("Existing change")));
3512        assert!(lines.iter().any(|l| l.contains("Alice's change")));
3513    }
3514
3515    #[test]
3516    fn test_add_changes_for_author_with_existing_sections() {
3517        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3518
3519  [ Author 1 ]
3520  * Change by Author 1
3521
3522 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3523"#
3524        .parse()
3525        .unwrap();
3526
3527        entry
3528            .try_add_changes_for_author("Alice", vec!["* Alice's new change"])
3529            .unwrap();
3530
3531        let lines: Vec<_> = entry.change_lines().collect();
3532
3533        // Should have Author 1's section
3534        assert!(lines.iter().any(|l| l.contains("[ Author 1 ]")));
3535        // Should have added Alice's section
3536        assert!(lines.iter().any(|l| l.contains("[ Alice ]")));
3537        // Should have both changes
3538        assert!(lines.iter().any(|l| l.contains("Change by Author 1")));
3539        assert!(lines.iter().any(|l| l.contains("Alice's new change")));
3540    }
3541
3542    #[test]
3543    fn test_add_changes_for_author_same_author() {
3544        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3545
3546  [ Alice ]
3547  * First change
3548
3549 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3550"#
3551        .parse()
3552        .unwrap();
3553
3554        entry
3555            .try_add_changes_for_author("Alice", vec!["* Second change"])
3556            .unwrap();
3557
3558        let lines: Vec<_> = entry.change_lines().collect();
3559
3560        // Should only have one Alice section (not duplicated)
3561        let alice_count = lines.iter().filter(|l| l.contains("[ Alice ]")).count();
3562        assert_eq!(alice_count, 1);
3563
3564        // Should have both changes
3565        assert!(lines.iter().any(|l| l.contains("First change")));
3566        assert!(lines.iter().any(|l| l.contains("Second change")));
3567    }
3568
3569    #[test]
3570    fn test_datetime_with_incorrect_day_of_week() {
3571        // Test for bug: datetime() should parse leniently even when day-of-week doesn't match
3572        // This changelog entry has "Mon, 22 Mar 2011" but Mar 22, 2011 was actually Tuesday
3573        let entry: Entry = r#"blah (0.1-2) UNRELEASED; urgency=medium
3574
3575  * New release.
3576
3577 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 22 Mar 2011 16:47:42 +0000
3578"#
3579        .parse()
3580        .unwrap();
3581
3582        // timestamp() should return just the date portion
3583        assert_eq!(
3584            entry.timestamp(),
3585            Some("Mon, 22 Mar 2011 16:47:42 +0000".into())
3586        );
3587
3588        // datetime() should successfully parse the timestamp despite incorrect day-of-week
3589        let datetime = entry.datetime();
3590        assert!(
3591            datetime.is_some(),
3592            "datetime() should not return None for timestamp with incorrect day-of-week"
3593        );
3594        assert_eq!(datetime.unwrap().to_rfc3339(), "2011-03-22T16:47:42+00:00");
3595    }
3596
3597    #[test]
3598    fn test_line_col() {
3599        let text = r#"foo (1.0-1) unstable; urgency=low
3600
3601  * First change
3602
3603 -- Maintainer <email@example.com>  Mon, 01 Jan 2024 12:00:00 +0000
3604
3605bar (2.0-1) experimental; urgency=high
3606
3607  * Second change
3608  * Third change
3609
3610 -- Another <another@example.com>  Tue, 02 Jan 2024 13:00:00 +0000
3611"#;
3612        let changelog = text.parse::<ChangeLog>().unwrap();
3613
3614        // Test changelog root position
3615        assert_eq!(changelog.line(), 0);
3616        assert_eq!(changelog.column(), 0);
3617        assert_eq!(changelog.line_col(), (0, 0));
3618
3619        // Test entry line numbers
3620        let entries: Vec<_> = changelog.iter().collect();
3621        assert_eq!(entries.len(), 2);
3622
3623        // First entry starts at line 0
3624        assert_eq!(entries[0].line(), 0);
3625        assert_eq!(entries[0].column(), 0);
3626        assert_eq!(entries[0].line_col(), (0, 0));
3627
3628        // Second entry starts at line 6 (after first entry and empty line)
3629        assert_eq!(entries[1].line(), 6);
3630        assert_eq!(entries[1].column(), 0);
3631        assert_eq!(entries[1].line_col(), (6, 0));
3632
3633        // Test entry components
3634        let header = entries[0].header().unwrap();
3635        assert_eq!(header.line(), 0);
3636        assert_eq!(header.column(), 0);
3637
3638        let body = entries[0].body().unwrap();
3639        assert_eq!(body.line(), 2); // Body starts at first change line
3640
3641        let footer = entries[0].footer().unwrap();
3642        assert_eq!(footer.line(), 4); // Footer line
3643
3644        // Test maintainer and timestamp nodes
3645        let maintainer = entries[0].maintainer_node().unwrap();
3646        assert_eq!(maintainer.line(), 4); // On footer line
3647
3648        let timestamp = entries[0].timestamp_node().unwrap();
3649        assert_eq!(timestamp.line(), 4); // On footer line
3650
3651        // Verify second entry components
3652        let header2 = entries[1].header().unwrap();
3653        assert_eq!(header2.line(), 6);
3654
3655        let footer2 = entries[1].footer().unwrap();
3656        assert_eq!(footer2.line(), 11);
3657    }
3658}