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    #[deprecated(
1147        since = "0.2.10",
1148        note = "Use try_auto_add_change for proper error handling"
1149    )]
1150    pub fn auto_add_change(
1151        &mut self,
1152        change: &[&str],
1153        author: (String, String),
1154        datetime: Option<chrono::DateTime<chrono::FixedOffset>>,
1155        urgency: Option<Urgency>,
1156    ) -> Entry {
1157        self.try_auto_add_change(change, author, datetime, urgency)
1158            .unwrap()
1159    }
1160
1161    /// Pop the first entry from the changelog.
1162    pub fn pop_first(&mut self) -> Option<Entry> {
1163        let mut it = self.iter();
1164        if let Some(entry) = it.next() {
1165            // Drop trailing newlines
1166            while let Some(sibling) = entry.0.next_sibling() {
1167                if sibling.kind() == EMPTY_LINE {
1168                    sibling.detach();
1169                } else {
1170                    break;
1171                }
1172            }
1173            entry.0.detach();
1174            Some(entry)
1175        } else {
1176            None
1177        }
1178    }
1179
1180    /// Read a changelog file from a path
1181    pub fn read_path(path: impl AsRef<std::path::Path>) -> Result<ChangeLog, Error> {
1182        let mut file = std::fs::File::open(path)?;
1183        Self::read(&mut file)
1184    }
1185
1186    /// Read a changelog file from a reader
1187    pub fn read<R: std::io::Read>(mut r: R) -> Result<ChangeLog, Error> {
1188        let mut buf = String::new();
1189        r.read_to_string(&mut buf)?;
1190        Ok(buf.parse()?)
1191    }
1192
1193    /// Read a changelog file from a reader, allowing for syntax errors
1194    pub fn read_relaxed<R: std::io::Read>(mut r: R) -> Result<ChangeLog, Error> {
1195        let mut buf = String::new();
1196        r.read_to_string(&mut buf)?;
1197
1198        let parsed = parse(&buf);
1199        // For relaxed parsing, we ignore errors and return the tree anyway
1200        let node = SyntaxNode::new_root_mut(parsed.green().clone());
1201        Ok(ChangeLog::cast(node).expect("root node has wrong type"))
1202    }
1203
1204    /// Write the changelog to a writer
1205    pub fn write<W: std::io::Write>(&self, mut w: W) -> Result<(), Error> {
1206        let buf = self.to_string();
1207        w.write_all(buf.as_bytes())?;
1208        Ok(())
1209    }
1210
1211    /// Write the changelog to a path
1212    pub fn write_to_path(&self, p: &std::path::Path) -> Result<(), Error> {
1213        let f = std::fs::File::create(p)?;
1214        self.write(f)?;
1215        Ok(())
1216    }
1217
1218    /// Iterator over entries grouped by their maintainer (author).
1219    ///
1220    /// Returns an iterator over tuples of (maintainer_name, maintainer_email, Vec<Entry>)
1221    /// where entries with the same maintainer are grouped together.
1222    pub fn iter_by_author(&self) -> impl Iterator<Item = (String, String, Vec<Entry>)> + '_ {
1223        crate::iter_entries_by_author(self)
1224    }
1225
1226    /// Get all unique authors across all entries in the changelog.
1227    ///
1228    /// This includes both maintainers from entry footers and authors from [ Author Name ] sections.
1229    pub fn get_all_authors(&self) -> std::collections::HashSet<crate::Identity> {
1230        let mut authors = std::collections::HashSet::new();
1231
1232        // Add maintainers from entry footers
1233        for entry in self.iter() {
1234            if let Some(identity) = entry.get_maintainer_identity() {
1235                authors.insert(identity);
1236            }
1237        }
1238
1239        // Add authors from change sections
1240        for entry in self.iter() {
1241            for author_name in entry.get_authors() {
1242                // Create identity with empty email since we only have names from change sections
1243                authors.insert(crate::Identity::new(author_name, "".to_string()));
1244            }
1245        }
1246
1247        authors
1248    }
1249}
1250
1251impl Default for ChangeLog {
1252    fn default() -> Self {
1253        Self::new()
1254    }
1255}
1256
1257impl FromStr for ChangeLog {
1258    type Err = ParseError;
1259
1260    fn from_str(s: &str) -> Result<Self, Self::Err> {
1261        ChangeLog::parse(s).to_mut_result()
1262    }
1263}
1264
1265impl FromStr for Entry {
1266    type Err = ParseError;
1267
1268    fn from_str(s: &str) -> Result<Self, Self::Err> {
1269        let cl: ChangeLog = s.parse()?;
1270        let mut entries = cl.iter();
1271        let entry = entries
1272            .next()
1273            .ok_or_else(|| ParseError(vec!["no entries found".to_string()]))?;
1274        if entries.next().is_some() {
1275            return Err(ParseError(vec!["multiple entries found".to_string()]));
1276        }
1277        Ok(entry)
1278    }
1279}
1280
1281impl EntryHeader {
1282    /// Returns the version of the entry.
1283    pub fn version(&self) -> Option<Version> {
1284        self.0.children_with_tokens().find_map(|it| {
1285            if let Some(token) = it.as_token() {
1286                if token.kind() == VERSION {
1287                    let text = token.text()[1..token.text().len() - 1].to_string();
1288                    return Some(text.parse().unwrap());
1289                }
1290            }
1291            None
1292        })
1293    }
1294
1295    /// Returns the package name of the entry.
1296    pub fn package(&self) -> Option<String> {
1297        self.0.children_with_tokens().find_map(|it| {
1298            if let Some(token) = it.as_token() {
1299                if token.kind() == IDENTIFIER {
1300                    return Some(token.text().to_string());
1301                }
1302            }
1303            None
1304        })
1305    }
1306
1307    /// Returns the distributions of the entry.
1308    pub fn distributions(&self) -> Option<Vec<String>> {
1309        let node = self.0.children().find(|it| it.kind() == DISTRIBUTIONS);
1310
1311        node.map(|node| {
1312            node.children_with_tokens()
1313                .filter_map(|it| {
1314                    if let Some(token) = it.as_token() {
1315                        if token.kind() == IDENTIFIER {
1316                            return Some(token.text().to_string());
1317                        }
1318                    }
1319                    None
1320                })
1321                .collect::<Vec<_>>()
1322        })
1323    }
1324
1325    /// Set distributions for the entry.
1326    pub fn set_distributions(&mut self, _distributions: Vec<String>) {
1327        let node = self
1328            .0
1329            .children_with_tokens()
1330            .find(|it| it.kind() == DISTRIBUTIONS);
1331        let mut builder = GreenNodeBuilder::new();
1332        builder.start_node(DISTRIBUTIONS.into());
1333        for (i, distribution) in _distributions.iter().enumerate() {
1334            if i > 0 {
1335                builder.token(WHITESPACE.into(), " ");
1336            }
1337            builder.token(IDENTIFIER.into(), distribution);
1338        }
1339        builder.finish_node();
1340
1341        let (range, green) = if let Some(node) = node {
1342            (
1343                node.index()..node.index() + 1,
1344                vec![builder.finish().into()],
1345            )
1346        } else if let Some(version) = self
1347            .0
1348            .children_with_tokens()
1349            .find(|it| it.kind() == VERSION)
1350        {
1351            (
1352                version.index()..version.index() + 1,
1353                vec![
1354                    GreenToken::new(WHITESPACE.into(), " ").into(),
1355                    builder.finish().into(),
1356                ],
1357            )
1358        } else if let Some(metadata) = self
1359            .0
1360            .children_with_tokens()
1361            .find(|it| it.kind() == METADATA)
1362        {
1363            (
1364                metadata.index() - 1..metadata.index() - 1,
1365                vec![
1366                    GreenToken::new(WHITESPACE.into(), " ").into(),
1367                    builder.finish().into(),
1368                ],
1369            )
1370        } else {
1371            (
1372                self.0.children().count()..self.0.children().count(),
1373                vec![
1374                    GreenToken::new(WHITESPACE.into(), " ").into(),
1375                    builder.finish().into(),
1376                ],
1377            )
1378        };
1379
1380        let new_root = SyntaxNode::new_root_mut(self.0.green().splice_children(range, green));
1381        self.replace_root(new_root);
1382    }
1383
1384    /// Set the version for the entry.
1385    pub fn set_version(&mut self, version: &Version) {
1386        // Find the version token
1387        let node = self
1388            .0
1389            .children_with_tokens()
1390            .find(|it| it.kind() == VERSION);
1391        let (range, green) = if let Some(token) = node {
1392            (
1393                token.index()..token.index() + 1,
1394                vec![GreenToken::new(VERSION.into(), &format!("({})", version)).into()],
1395            )
1396        } else {
1397            let index = self
1398                .0
1399                .children_with_tokens()
1400                .position(|it| it.kind() == IDENTIFIER)
1401                .unwrap_or(0);
1402            (
1403                index + 1..index + 1,
1404                vec![
1405                    GreenToken::new(WHITESPACE.into(), " ").into(),
1406                    GreenToken::new(VERSION.into(), &format!("({})", version)).into(),
1407                ],
1408            )
1409        };
1410        let new_root = SyntaxNode::new_root_mut(self.0.green().splice_children(range, green));
1411
1412        self.replace_root(new_root);
1413    }
1414
1415    /// Set the package name for the entry.
1416    pub fn set_package(&mut self, package: String) {
1417        let node = self
1418            .0
1419            .children_with_tokens()
1420            .find(|it| it.kind() == IDENTIFIER);
1421
1422        let new_root = if let Some(token) = node {
1423            SyntaxNode::new_root_mut(self.0.green().splice_children(
1424                token.index()..token.index() + 1,
1425                vec![GreenToken::new(IDENTIFIER.into(), &package).into()],
1426            ))
1427        } else {
1428            SyntaxNode::new_root_mut(self.0.green().splice_children(
1429                0..0,
1430                vec![
1431                    GreenToken::new(IDENTIFIER.into(), &package).into(),
1432                    GreenToken::new(WHITESPACE.into(), " ").into(),
1433                ],
1434            ))
1435        };
1436
1437        self.replace_root(new_root);
1438    }
1439
1440    /// Set extra metadata for the entry.
1441    pub fn set_metadata(&mut self, key: &str, value: &str) {
1442        // Find the appropriate metadata node
1443        if let Some(mut node) = self
1444            .metadata_nodes()
1445            .find(|it| it.key().map(|k| k == key).unwrap_or(false))
1446        {
1447            node.set_value(value);
1448        } else if let Some(metadata) = self
1449            .0
1450            .children_with_tokens()
1451            .find(|it| it.kind() == METADATA)
1452        {
1453            let mut builder = GreenNodeBuilder::new();
1454            builder.start_node(METADATA_ENTRY.into());
1455            builder.start_node(METADATA_KEY.into());
1456            builder.token(IDENTIFIER.into(), key);
1457            builder.finish_node();
1458            builder.token(EQUALS.into(), "=");
1459            builder.start_node(METADATA_VALUE.into());
1460            builder.token(IDENTIFIER.into(), value);
1461            builder.finish_node();
1462            builder.finish_node();
1463
1464            let metadata = metadata.as_node().unwrap();
1465
1466            let count = metadata.children_with_tokens().count();
1467            self.0.splice_children(
1468                metadata.index()..metadata.index() + 1,
1469                vec![SyntaxNode::new_root_mut(metadata.green().splice_children(
1470                    count..count,
1471                    vec![
1472                        GreenToken::new(WHITESPACE.into(), " ").into(),
1473                        builder.finish().into(),
1474                    ],
1475                ))
1476                .into()],
1477            );
1478        } else {
1479            let mut builder = GreenNodeBuilder::new();
1480            builder.start_node(METADATA.into());
1481            builder.token(SEMICOLON.into(), ";");
1482            builder.token(WHITESPACE.into(), " ");
1483            builder.start_node(METADATA_ENTRY.into());
1484            builder.start_node(METADATA_KEY.into());
1485            builder.token(IDENTIFIER.into(), key);
1486            builder.finish_node();
1487            builder.token(EQUALS.into(), "=");
1488            builder.start_node(METADATA_VALUE.into());
1489            builder.token(IDENTIFIER.into(), value);
1490            builder.finish_node();
1491            builder.finish_node();
1492
1493            let new_root = SyntaxNode::new_root_mut(builder.finish());
1494
1495            // Add either just after DISTRIBUTIONS
1496            if let Some(distributions) = self
1497                .0
1498                .children_with_tokens()
1499                .find(|it| it.kind() == DISTRIBUTIONS)
1500            {
1501                self.0.splice_children(
1502                    distributions.index() + 1..distributions.index() + 1,
1503                    vec![new_root.into()],
1504                );
1505            } else if let Some(nl) = self
1506                .0
1507                .children_with_tokens()
1508                .find(|it| it.kind() == NEWLINE)
1509            {
1510                // Just before the newline
1511                self.0
1512                    .splice_children(nl.index()..nl.index(), vec![new_root.into()]);
1513            } else {
1514                let count = self.0.children_with_tokens().count();
1515                self.0.splice_children(count..count, vec![new_root.into()]);
1516            }
1517        }
1518    }
1519
1520    /// Returns an iterator over the metadata entry AST nodes.
1521    pub fn metadata_nodes(&self) -> impl Iterator<Item = MetadataEntry> + '_ {
1522        let node = self.0.children().find(|it| it.kind() == METADATA);
1523
1524        node.into_iter().flat_map(|node| {
1525            node.children_with_tokens()
1526                .filter_map(|it| MetadataEntry::cast(it.into_node()?))
1527        })
1528    }
1529
1530    /// Returns an iterator over the metadata key-value pairs.
1531    pub fn metadata(&self) -> impl Iterator<Item = (String, String)> + '_ {
1532        self.metadata_nodes().filter_map(|entry| {
1533            if let (Some(key), Some(value)) = (entry.key(), entry.value()) {
1534                Some((key, value))
1535            } else {
1536                None
1537            }
1538        })
1539    }
1540
1541    /// Returns the urgency of the entry.3
1542    pub fn urgency(&self) -> Option<Urgency> {
1543        for (key, value) in self.metadata() {
1544            if key.as_str() == "urgency" {
1545                return Some(value.parse().unwrap());
1546            }
1547        }
1548        None
1549    }
1550}
1551
1552impl EntryFooter {
1553    /// Returns the email address of the maintainer from the footer.
1554    pub fn email(&self) -> Option<String> {
1555        self.0.children_with_tokens().find_map(|it| {
1556            if let Some(token) = it.as_token() {
1557                let text = token.text();
1558                if token.kind() == EMAIL {
1559                    return Some(text[1..text.len() - 1].to_string());
1560                }
1561            }
1562            None
1563        })
1564    }
1565
1566    /// Returns the maintainer name from the footer.
1567    pub fn maintainer(&self) -> Option<String> {
1568        self.0
1569            .children()
1570            .find_map(Maintainer::cast)
1571            .map(|m| m.text())
1572            .filter(|s| !s.is_empty())
1573    }
1574
1575    /// Set the maintainer for the entry.
1576    pub fn set_maintainer(&mut self, maintainer: String) {
1577        let node = self
1578            .0
1579            .children_with_tokens()
1580            .find(|it| it.kind() == MAINTAINER);
1581        let new_root = if let Some(node) = node {
1582            SyntaxNode::new_root_mut(self.0.green().splice_children(
1583                node.index()..node.index() + 1,
1584                vec![GreenToken::new(MAINTAINER.into(), &maintainer).into()],
1585            ))
1586        } else if let Some(node) = self.0.children_with_tokens().find(|it| it.kind() == INDENT) {
1587            SyntaxNode::new_root_mut(self.0.green().splice_children(
1588                node.index() + 1..node.index() + 1,
1589                vec![GreenToken::new(MAINTAINER.into(), &maintainer).into()],
1590            ))
1591        } else {
1592            SyntaxNode::new_root_mut(self.0.green().splice_children(
1593                0..0,
1594                vec![
1595                    GreenToken::new(INDENT.into(), " -- ").into(),
1596                    GreenToken::new(MAINTAINER.into(), &maintainer).into(),
1597                ],
1598            ))
1599        };
1600
1601        self.replace_root(new_root);
1602    }
1603
1604    /// Set email for the entry.
1605    pub fn set_email(&mut self, _email: String) {
1606        let node = self.0.children_with_tokens().find(|it| it.kind() == EMAIL);
1607        let new_root = if let Some(node) = node {
1608            SyntaxNode::new_root_mut(self.0.green().splice_children(
1609                node.index()..node.index() + 1,
1610                vec![GreenToken::new(EMAIL.into(), &format!("<{}>", _email)).into()],
1611            ))
1612        } else if let Some(node) = self
1613            .0
1614            .children_with_tokens()
1615            .find(|it| it.kind() == MAINTAINER)
1616        {
1617            SyntaxNode::new_root_mut(self.0.green().splice_children(
1618                node.index() + 1..node.index() + 1,
1619                vec![GreenToken::new(EMAIL.into(), &format!("<{}>", _email)).into()],
1620            ))
1621        } else if let Some(node) = self.0.children_with_tokens().find(|it| it.kind() == INDENT) {
1622            SyntaxNode::new_root_mut(self.0.green().splice_children(
1623                node.index() + 1..node.index() + 1,
1624                vec![
1625                    GreenToken::new(MAINTAINER.into(), "").into(),
1626                    GreenToken::new(WHITESPACE.into(), " ").into(),
1627                    GreenToken::new(EMAIL.into(), &format!("<{}>", _email)).into(),
1628                ],
1629            ))
1630        } else {
1631            SyntaxNode::new_root_mut(self.0.green().splice_children(
1632                0..0,
1633                vec![
1634                    GreenToken::new(INDENT.into(), " -- ").into(),
1635                    GreenToken::new(MAINTAINER.into(), "").into(),
1636                    GreenToken::new(WHITESPACE.into(), " ").into(),
1637                    GreenToken::new(EMAIL.into(), &format!("<{}>", _email)).into(),
1638                ],
1639            ))
1640        };
1641
1642        self.replace_root(new_root);
1643    }
1644
1645    /// Returns the timestamp from the footer.
1646    pub fn timestamp(&self) -> Option<String> {
1647        self.0
1648            .children()
1649            .find_map(Timestamp::cast)
1650            .map(|m| m.text())
1651    }
1652
1653    /// Set timestamp for the entry.
1654    pub fn set_timestamp(&mut self, timestamp: String) {
1655        let node = self
1656            .0
1657            .children_with_tokens()
1658            .find(|it| it.kind() == TIMESTAMP);
1659        let new_root = if let Some(node) = node {
1660            SyntaxNode::new_root_mut(self.0.green().splice_children(
1661                node.index()..node.index() + 1,
1662                vec![GreenToken::new(TIMESTAMP.into(), &timestamp).into()],
1663            ))
1664        } else if let Some(node) = self.0.children_with_tokens().find(|it| it.kind() == INDENT) {
1665            SyntaxNode::new_root_mut(self.0.green().splice_children(
1666                node.index() + 1..node.index() + 1,
1667                vec![GreenToken::new(TIMESTAMP.into(), &timestamp).into()],
1668            ))
1669        } else if let Some(node) = self.0.children_with_tokens().find(|it| it.kind() == EMAIL) {
1670            SyntaxNode::new_root_mut(self.0.green().splice_children(
1671                node.index() + 1..node.index() + 1,
1672                vec![GreenToken::new(TIMESTAMP.into(), &timestamp).into()],
1673            ))
1674        } else {
1675            SyntaxNode::new_root_mut(self.0.green().splice_children(
1676                0..0,
1677                vec![
1678                    GreenToken::new(INDENT.into(), " -- ").into(),
1679                    GreenToken::new(TIMESTAMP.into(), &timestamp).into(),
1680                ],
1681            ))
1682        };
1683
1684        self.replace_root(new_root);
1685    }
1686}
1687
1688impl EntryBody {
1689    fn text(&self) -> String {
1690        self.0
1691            .children_with_tokens()
1692            .filter_map(|it| {
1693                if let Some(token) = it.as_token() {
1694                    if token.kind() == DETAIL {
1695                        return Some(token.text().to_string());
1696                    }
1697                }
1698                None
1699            })
1700            .collect::<Vec<_>>()
1701            .concat()
1702    }
1703}
1704
1705impl Timestamp {
1706    fn text(&self) -> String {
1707        self.0.text().to_string()
1708    }
1709}
1710
1711impl Maintainer {
1712    fn text(&self) -> String {
1713        self.0.text().to_string()
1714    }
1715}
1716
1717impl Entry {
1718    /// Returns the header AST node of the entry.
1719    pub fn header(&self) -> Option<EntryHeader> {
1720        self.0.children().find_map(EntryHeader::cast)
1721    }
1722
1723    /// Returns the body AST node of the entry.
1724    pub fn body(&self) -> Option<EntryBody> {
1725        self.0.children().find_map(EntryBody::cast)
1726    }
1727
1728    /// Returns the footer AST node of the entry.
1729    pub fn footer(&self) -> Option<EntryFooter> {
1730        self.0.children().find_map(EntryFooter::cast)
1731    }
1732
1733    /// Return the package name of the entry.
1734    pub fn package(&self) -> Option<String> {
1735        self.header().and_then(|h| h.package())
1736    }
1737
1738    /// Set the package name of the entry.
1739    pub fn set_package(&mut self, package: String) {
1740        if let Some(mut header) = self.header() {
1741            let header_index = header.0.index();
1742            header.set_package(package);
1743            self.0
1744                .splice_children(header_index..header_index + 1, vec![header.0.into()]);
1745        } else {
1746            self.create_header().set_package(package);
1747        }
1748    }
1749
1750    /// Return the version of the entry.
1751    pub fn version(&self) -> Option<Version> {
1752        self.header().and_then(|h| h.version())
1753    }
1754
1755    /// Set the version of the entry.
1756    pub fn set_version(&mut self, version: &Version) {
1757        if let Some(mut header) = self.header() {
1758            let header_index = header.0.index();
1759            header.set_version(version);
1760            self.0
1761                .splice_children(header_index..header_index + 1, vec![header.0.into()]);
1762        } else {
1763            self.create_header().set_version(version);
1764        }
1765    }
1766
1767    /// Return the distributions of the entry.
1768    pub fn distributions(&self) -> Option<Vec<String>> {
1769        self.header().and_then(|h| h.distributions())
1770    }
1771
1772    /// Set the distributions for the entry
1773    pub fn set_distributions(&mut self, distributions: Vec<String>) {
1774        if let Some(mut header) = self.header() {
1775            let header_index = header.0.index();
1776            header.set_distributions(distributions);
1777            self.0
1778                .splice_children(header_index..header_index + 1, vec![header.0.into()]);
1779        } else {
1780            self.create_header().set_distributions(distributions);
1781        }
1782    }
1783
1784    /// Returns the email address of the maintainer.
1785    pub fn email(&self) -> Option<String> {
1786        self.footer().and_then(|f| f.email())
1787    }
1788
1789    /// Returns the maintainer AST node.
1790    pub fn maintainer_node(&self) -> Option<Maintainer> {
1791        self.footer()
1792            .and_then(|f| f.0.children().find_map(Maintainer::cast))
1793    }
1794
1795    /// Returns the name of the maintainer.
1796    pub fn maintainer(&self) -> Option<String> {
1797        self.footer().and_then(|f| f.maintainer())
1798    }
1799
1800    /// Set the maintainer of the entry.
1801    pub fn set_maintainer(&mut self, maintainer: (String, String)) {
1802        if let Some(mut footer) = self.footer() {
1803            let footer_index = footer.0.index();
1804            footer.set_maintainer(maintainer.0);
1805            footer.set_email(maintainer.1);
1806            self.0
1807                .splice_children(footer_index..footer_index + 1, vec![footer.0.into()]);
1808        } else {
1809            let mut footer = self.create_footer();
1810            footer.set_maintainer(maintainer.0);
1811            footer.set_email(maintainer.1);
1812        }
1813    }
1814
1815    /// Returns the timestamp AST node.
1816    pub fn timestamp_node(&self) -> Option<Timestamp> {
1817        self.footer()
1818            .and_then(|f| f.0.children().find_map(Timestamp::cast))
1819    }
1820
1821    /// Returns the timestamp of the entry, as the raw string.
1822    pub fn timestamp(&self) -> Option<String> {
1823        self.footer().and_then(|f| f.timestamp())
1824    }
1825
1826    /// Set the timestamp of the entry.
1827    pub fn set_timestamp(&mut self, timestamp: String) {
1828        if let Some(mut footer) = self.footer() {
1829            let footer_index = footer.0.index();
1830            footer.set_timestamp(timestamp);
1831            self.0
1832                .splice_children(footer_index..footer_index + 1, vec![footer.0.into()]);
1833        } else {
1834            self.create_footer().set_timestamp(timestamp);
1835        }
1836    }
1837
1838    /// Set the datetime of the entry.
1839    #[cfg(feature = "chrono")]
1840    pub fn set_datetime(&mut self, datetime: DateTime<FixedOffset>) {
1841        self.set_timestamp(format!("{}", datetime.format("%a, %d %b %Y %H:%M:%S %z")));
1842    }
1843
1844    /// Returns the datetime of the entry.
1845    #[cfg(feature = "chrono")]
1846    pub fn datetime(&self) -> Option<DateTime<FixedOffset>> {
1847        self.timestamp().and_then(|ts| parse_time_string(&ts).ok())
1848    }
1849
1850    /// Returns the urgency of the entry.
1851    pub fn urgency(&self) -> Option<Urgency> {
1852        self.header().and_then(|h| h.urgency())
1853    }
1854
1855    fn create_header(&self) -> EntryHeader {
1856        let mut builder = GreenNodeBuilder::new();
1857        builder.start_node(ENTRY_HEADER.into());
1858        builder.token(NEWLINE.into(), "\n");
1859        builder.finish_node();
1860        let syntax = SyntaxNode::new_root_mut(builder.finish());
1861        self.0.splice_children(0..0, vec![syntax.into()]);
1862        EntryHeader(self.0.children().next().unwrap().clone_for_update())
1863    }
1864
1865    fn create_footer(&self) -> EntryFooter {
1866        let mut builder = GreenNodeBuilder::new();
1867        builder.start_node(ENTRY_FOOTER.into());
1868        builder.token(NEWLINE.into(), "\n");
1869        builder.finish_node();
1870        let syntax = SyntaxNode::new_root_mut(builder.finish());
1871        let count = self.0.children().count();
1872        self.0.splice_children(count..count, vec![syntax.into()]);
1873        EntryFooter(self.0.children().last().unwrap().clone_for_update())
1874    }
1875
1876    /// Set the urgency of the entry.
1877    pub fn set_urgency(&mut self, urgency: Urgency) {
1878        self.set_metadata("urgency", urgency.to_string().as_str());
1879    }
1880
1881    /// Set a metadata key-value pair for the entry.
1882    pub fn set_metadata(&mut self, key: &str, value: &str) {
1883        if let Some(mut header) = self.header() {
1884            let header_index = header.0.index();
1885            header.set_metadata(key, value);
1886            self.0
1887                .splice_children(header_index..header_index + 1, vec![header.0.into()]);
1888        } else {
1889            self.create_header().set_metadata(key, value);
1890        }
1891    }
1892
1893    /// Add a change for the specified author
1894    ///
1895    /// If the author is not the same as the current maintainer, a new
1896    /// section will be created for the author in the entry (e.g. "[ John Doe ]").
1897    ///
1898    /// Returns an error if text rewrapping fails.
1899    pub fn try_add_change_for_author(
1900        &self,
1901        change: &[&str],
1902        author: (String, String),
1903    ) -> Result<(), crate::textwrap::Error> {
1904        let changes_lines = self.change_lines().collect::<Vec<_>>();
1905        let by_author = crate::changes::changes_by_author(changes_lines.iter().map(|s| s.as_str()))
1906            .collect::<Vec<_>>();
1907
1908        // There are no per author sections yet, so attribute current changes to changelog entry author
1909        if by_author.iter().all(|(a, _, _)| a.is_none()) {
1910            if let Some(maintainer_name) = self.maintainer() {
1911                if author.0 != maintainer_name {
1912                    self.prepend_change_line(
1913                        crate::changes::format_section_title(maintainer_name.as_str()).as_str(),
1914                    );
1915                    if !self.change_lines().last().unwrap().is_empty() {
1916                        self.append_change_line("");
1917                    }
1918                    self.append_change_line(
1919                        crate::changes::format_section_title(author.0.as_str()).as_str(),
1920                    );
1921                }
1922            }
1923        } else if let Some(last_section) = by_author.last().as_ref() {
1924            if last_section.0 != Some(author.0.as_str()) {
1925                self.append_change_line("");
1926                self.append_change_line(
1927                    crate::changes::format_section_title(author.0.as_str()).as_str(),
1928                );
1929            }
1930        }
1931
1932        if let Some(last) = self.change_lines().last() {
1933            if last.trim().is_empty() {
1934                self.pop_change_line();
1935            }
1936        }
1937
1938        for line in crate::textwrap::try_rewrap_changes(change.iter().copied())? {
1939            self.append_change_line(line.as_ref());
1940        }
1941        Ok(())
1942    }
1943
1944    /// Add a change for the specified author
1945    ///
1946    /// If the author is not the same as the current maintainer, a new
1947    /// section will be created for the author in the entry (e.g. "[ John Doe ]").
1948    ///
1949    /// # Deprecated
1950    ///
1951    /// This function panics on errors. Use [`Entry::try_add_change_for_author`] instead for proper error handling.
1952    ///
1953    /// # Panics
1954    ///
1955    /// Panics if text rewrapping fails.
1956    #[deprecated(
1957        since = "0.2.10",
1958        note = "Use try_add_change_for_author for proper error handling"
1959    )]
1960    pub fn add_change_for_author(&self, change: &[&str], author: (String, String)) {
1961        self.try_add_change_for_author(change, author).unwrap()
1962    }
1963
1964    /// Prepend a change line to the entry
1965    pub fn prepend_change_line(&self, line: &str) {
1966        let mut builder = GreenNodeBuilder::new();
1967        builder.start_node(ENTRY_BODY.into());
1968        if !line.is_empty() {
1969            builder.token(INDENT.into(), "  ");
1970            builder.token(DETAIL.into(), line);
1971        }
1972        builder.token(NEWLINE.into(), "\n");
1973        builder.finish_node();
1974
1975        // Insert just after the header
1976        let mut it = self.0.children();
1977        let header = it.find(|n| n.kind() == ENTRY_HEADER);
1978
1979        let previous_line = it.find(|n| n.kind() == EMPTY_LINE).or(header);
1980
1981        let index = previous_line.map_or(0, |l| l.index() + 1);
1982
1983        let syntax = SyntaxNode::new_root_mut(builder.finish());
1984
1985        self.0.splice_children(index..index, vec![syntax.into()]);
1986    }
1987
1988    /// Pop the last change line from the entry
1989    pub fn pop_change_line(&self) -> Option<String> {
1990        // Find the last child of type ENTRY_BODY
1991        let last_child = self.0.children().filter(|n| n.kind() == ENTRY_BODY).last();
1992
1993        if let Some(last_child) = last_child {
1994            let text = last_child.children_with_tokens().find_map(|it| {
1995                if let Some(token) = it.as_token() {
1996                    if token.kind() == DETAIL {
1997                        return Some(token.text().to_string());
1998                    }
1999                }
2000                None
2001            });
2002            self.0
2003                .splice_children(last_child.index()..last_child.index() + 1, vec![]);
2004            text
2005        } else {
2006            None
2007        }
2008    }
2009
2010    /// Append a line to the changelog entry
2011    pub fn append_change_line(&self, line: &str) {
2012        let mut builder = GreenNodeBuilder::new();
2013        builder.start_node(ENTRY_BODY.into());
2014        if !line.is_empty() {
2015            builder.token(INDENT.into(), "  ");
2016            builder.token(DETAIL.into(), line);
2017        }
2018        builder.token(NEWLINE.into(), "\n");
2019        builder.finish_node();
2020
2021        // Find the last child of type ENTRY_BODY
2022        let last_child = self
2023            .0
2024            .children()
2025            .filter(|n| n.kind() == ENTRY_BODY)
2026            .last()
2027            .unwrap_or_else(|| {
2028                // No ENTRY_BODY nodes exist. Insert after the EMPTY_LINE that follows
2029                // the ENTRY_HEADER (if it exists), to preserve required blank line.
2030                let children: Vec<_> = self.0.children().collect();
2031                if children.len() >= 2
2032                    && children[0].kind() == ENTRY_HEADER
2033                    && children[1].kind() == EMPTY_LINE
2034                {
2035                    children[1].clone()
2036                } else {
2037                    children[0].clone()
2038                }
2039            });
2040
2041        let syntax = SyntaxNode::new_root_mut(builder.finish()).into();
2042        self.0
2043            .splice_children(last_child.index() + 1..last_child.index() + 1, vec![syntax]);
2044    }
2045
2046    /// Add a bullet point to the changelog entry.
2047    ///
2048    /// This is a convenience method that appends a bullet point line to the entry.
2049    /// Always prepends "* " to the text and wraps the text to 78 columns if needed.
2050    ///
2051    /// # Arguments
2052    /// * `text` - The text of the bullet point (without the "* " prefix)
2053    ///
2054    /// # Examples
2055    /// ```
2056    /// use debian_changelog::ChangeLog;
2057    ///
2058    /// let mut changelog = ChangeLog::new();
2059    /// let entry = changelog.new_entry()
2060    ///     .maintainer(("Author".into(), "author@example.com".into()))
2061    ///     .distribution("unstable".to_string())
2062    ///     .version("1.0.0".parse().unwrap())
2063    ///     .finish();
2064    ///
2065    /// entry.add_bullet("First change");
2066    /// entry.add_bullet("Second change");
2067    ///
2068    /// let lines: Vec<_> = entry.change_lines().collect();
2069    /// assert_eq!(lines[0], "* First change");
2070    /// assert_eq!(lines[1], "* Second change");
2071    /// ```
2072    pub fn add_bullet(&self, text: &str) {
2073        // Wrap the text with "* " prefix
2074        let wrapped = crate::textwrap::textwrap(
2075            text,
2076            Some(crate::textwrap::DEFAULT_WIDTH),
2077            Some(crate::textwrap::INITIAL_INDENT),
2078            Some("  "),
2079        );
2080
2081        // Append each wrapped line
2082        for line in wrapped {
2083            self.append_change_line(&line);
2084        }
2085    }
2086
2087    /// Returns the changes of the entry.
2088    pub fn change_lines(&self) -> impl Iterator<Item = String> + '_ {
2089        let mut lines = self
2090            .0
2091            .children()
2092            .filter_map(|n| {
2093                if let Some(ref change) = EntryBody::cast(n.clone()) {
2094                    Some(change.text())
2095                } else if n.kind() == EMPTY_LINE {
2096                    Some("".to_string())
2097                } else {
2098                    None
2099                }
2100            })
2101            .collect::<Vec<_>>();
2102
2103        while let Some(last) = lines.last() {
2104            if last.is_empty() {
2105                lines.pop();
2106            } else {
2107                break;
2108            }
2109        }
2110
2111        lines.into_iter().skip_while(|it| it.is_empty())
2112    }
2113
2114    /// Ensure that the first line of the entry is the specified line
2115    ///
2116    /// If the first line is not the specified line, it will be prepended to the entry.
2117    pub fn ensure_first_line(&self, line: &str) {
2118        let first_line = self.change_lines().next().map(|it| it.trim().to_string());
2119
2120        if first_line != Some(line.to_string()) {
2121            self.prepend_change_line(line);
2122        }
2123    }
2124
2125    /// Return whether the entry is marked as being unreleased
2126    pub fn is_unreleased(&self) -> Option<bool> {
2127        let distro_is_unreleased = self.distributions().as_ref().map(|ds| {
2128            let ds = ds.iter().map(|d| d.as_str()).collect::<Vec<&str>>();
2129            crate::distributions_is_unreleased(ds.as_slice())
2130        });
2131
2132        let footer_is_unreleased = if self.maintainer().is_none() && self.email().is_none() {
2133            Some(true)
2134        } else {
2135            None
2136        };
2137
2138        match (distro_is_unreleased, footer_is_unreleased) {
2139            (Some(true), _) => Some(true),
2140            (_, Some(true)) => Some(true),
2141            (Some(false), _) => Some(false),
2142            (_, Some(false)) => Some(false),
2143            _ => None,
2144        }
2145    }
2146
2147    /// Iterator over changes in this entry grouped by author.
2148    ///
2149    /// Returns a vector of tuples (author_name, line_numbers, change_lines)
2150    /// where author_name is Some for attributed changes or None for changes without attribution.
2151    pub fn iter_changes_by_author(&self) -> Vec<(Option<String>, Vec<usize>, Vec<String>)> {
2152        let changes: Vec<String> = self.change_lines().map(|s| s.to_string()).collect();
2153        crate::changes::changes_by_author(changes.iter().map(|s| s.as_str()))
2154            .map(|(author, linenos, lines)| {
2155                let author_name = author.map(|s| s.to_string());
2156                let change_lines = lines.into_iter().map(|s| s.to_string()).collect();
2157                (author_name, linenos, change_lines)
2158            })
2159            .collect()
2160    }
2161
2162    /// Get all authors mentioned in this entry's changes.
2163    ///
2164    /// This includes authors from [ Author Name ] sections in the change text,
2165    /// but not the main maintainer/uploader from the entry footer.
2166    pub fn get_authors(&self) -> std::collections::HashSet<String> {
2167        let changes: Vec<String> = self.change_lines().map(|s| s.to_string()).collect();
2168        let change_strs: Vec<&str> = changes.iter().map(|s| s.as_str()).collect();
2169        crate::changes::find_extra_authors(&change_strs)
2170            .into_iter()
2171            .map(|s| s.to_string())
2172            .collect()
2173    }
2174
2175    /// Get the maintainer information as an Identity struct.
2176    ///
2177    /// Returns the maintainer name and email from the entry footer if available.
2178    pub fn get_maintainer_identity(&self) -> Option<crate::Identity> {
2179        if let (Some(name), Some(email)) = (self.maintainer(), self.email()) {
2180            Some(crate::Identity::new(name, email))
2181        } else {
2182            None
2183        }
2184    }
2185
2186    /// Add changes for a specific author to this entry.
2187    ///
2188    /// This will add an author section (e.g., `[ Author Name ]`) if needed,
2189    /// and append the changes under that section. If this is the first attributed
2190    /// change and there are existing unattributed changes, they will be wrapped
2191    /// in the maintainer's section.
2192    ///
2193    /// # Arguments
2194    /// * `author_name` - The name of the author to attribute the changes to
2195    /// * `changes` - A list of change lines to add (e.g., `["* Fixed bug"]`)
2196    ///
2197    /// # Example
2198    /// ```
2199    /// use debian_changelog::Entry;
2200    /// let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2201    ///
2202    ///   * Existing change
2203    ///
2204    ///  -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2205    /// "#.parse().unwrap();
2206    ///
2207    /// entry.try_add_changes_for_author("Alice", vec!["* New feature"]);
2208    /// ```
2209    pub fn try_add_changes_for_author(
2210        &self,
2211        author_name: &str,
2212        changes: Vec<&str>,
2213    ) -> Result<(), crate::textwrap::Error> {
2214        let mut change_lines: Vec<String> = self.change_lines().collect();
2215        let original_len = change_lines.len();
2216        let default_author = self.get_maintainer_identity().map(|id| (id.name, id.email));
2217
2218        crate::changes::try_add_change_for_author(
2219            &mut change_lines,
2220            author_name,
2221            changes,
2222            default_author,
2223        )?;
2224
2225        // The function modifies change_lines in place. We need to handle two cases:
2226        // 1. Lines were inserted at the beginning (when wrapping existing changes)
2227        // 2. Lines were appended at the end (normal case)
2228
2229        if change_lines.len() > original_len {
2230            // New lines were added
2231            let original_changes: Vec<_> = self.change_lines().collect();
2232
2233            // Check if lines were inserted at the start
2234            let inserted_at_start = original_len > 0 && change_lines[0] != original_changes[0];
2235
2236            if inserted_at_start {
2237                // Lines were inserted at the beginning - we need to rebuild
2238                // This happens when converting unattributed changes to attributed ones
2239                while self.pop_change_line().is_some() {}
2240                for line in change_lines {
2241                    self.append_change_line(&line);
2242                }
2243            } else {
2244                // Lines were appended at the end - just append the new ones
2245                for line in change_lines.iter().skip(original_len) {
2246                    self.append_change_line(line);
2247                }
2248            }
2249        }
2250        Ok(())
2251    }
2252
2253    /// Add changes for the specified author
2254    ///
2255    /// # Deprecated
2256    ///
2257    /// This function panics on errors. Use [`Entry::try_add_changes_for_author`] instead for proper error handling.
2258    ///
2259    /// # Panics
2260    ///
2261    /// Panics if text rewrapping fails.
2262    #[deprecated(
2263        since = "0.2.10",
2264        note = "Use try_add_changes_for_author for proper error handling"
2265    )]
2266    pub fn add_changes_for_author(&self, author_name: &str, changes: Vec<&str>) {
2267        self.try_add_changes_for_author(author_name, changes)
2268            .unwrap()
2269    }
2270}
2271
2272#[cfg(feature = "chrono")]
2273const CHANGELOG_TIME_FORMAT: &str = "%a, %d %b %Y %H:%M:%S %z";
2274
2275#[cfg(feature = "chrono")]
2276fn parse_time_string(time_str: &str) -> Result<DateTime<FixedOffset>, chrono::ParseError> {
2277    // First try parsing with day-of-week validation
2278    if let Ok(dt) = DateTime::parse_from_str(time_str, CHANGELOG_TIME_FORMAT) {
2279        return Ok(dt);
2280    }
2281
2282    // If that fails, try parsing without day-of-week validation
2283    // This is more lenient for changelogs that have incorrect day-of-week values
2284    // Skip the day name (everything before the first comma and space)
2285    if let Some(after_comma) = time_str.split_once(", ") {
2286        DateTime::parse_from_str(after_comma.1, "%d %b %Y %H:%M:%S %z")
2287    } else {
2288        // If there's no comma, return the original error
2289        DateTime::parse_from_str(time_str, CHANGELOG_TIME_FORMAT)
2290    }
2291}
2292
2293#[cfg(test)]
2294mod tests {
2295    use super::*;
2296
2297    #[test]
2298    fn test_parse_simple() {
2299        const CHANGELOG: &str = r#"breezy (3.3.4-1) unstable; urgency=low
2300
2301  * New upstream release.
2302
2303 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2304
2305breezy (3.3.3-2) unstable; urgency=medium
2306
2307  * Drop unnecessary dependency on python3-six. Closes: #1039011
2308  * Drop dependency on cython3-dbg. Closes: #1040544
2309
2310 -- Jelmer Vernooij <jelmer@debian.org>  Sat, 24 Jun 2023 14:58:57 +0100
2311
2312# Oh, and here is a comment
2313"#;
2314        let parsed = parse(CHANGELOG);
2315        assert_eq!(parsed.errors(), &Vec::<String>::new());
2316        let node = parsed.syntax_node();
2317        assert_eq!(
2318            format!("{:#?}", node),
2319            r###"ROOT@0..405
2320  ENTRY@0..140
2321    ENTRY_HEADER@0..39
2322      IDENTIFIER@0..6 "breezy"
2323      WHITESPACE@6..7 " "
2324      VERSION@7..16 "(3.3.4-1)"
2325      WHITESPACE@16..17 " "
2326      DISTRIBUTIONS@17..25
2327        IDENTIFIER@17..25 "unstable"
2328      METADATA@25..38
2329        SEMICOLON@25..26 ";"
2330        WHITESPACE@26..27 " "
2331        METADATA_ENTRY@27..38
2332          METADATA_KEY@27..34
2333            IDENTIFIER@27..34 "urgency"
2334          EQUALS@34..35 "="
2335          METADATA_VALUE@35..38
2336            IDENTIFIER@35..38 "low"
2337      NEWLINE@38..39 "\n"
2338    EMPTY_LINE@39..40
2339      NEWLINE@39..40 "\n"
2340    ENTRY_BODY@40..66
2341      INDENT@40..42 "  "
2342      DETAIL@42..65 "* New upstream release."
2343      NEWLINE@65..66 "\n"
2344    EMPTY_LINE@66..67
2345      NEWLINE@66..67 "\n"
2346    ENTRY_FOOTER@67..140
2347      INDENT@67..71 " -- "
2348      MAINTAINER@71..86
2349        TEXT@71..77 "Jelmer"
2350        WHITESPACE@77..78 " "
2351        TEXT@78..86 "Vernooij"
2352      WHITESPACE@86..87 " "
2353      EMAIL@87..106 "<jelmer@debian.org>"
2354      WHITESPACE@106..108 "  "
2355      TIMESTAMP@108..139
2356        TEXT@108..112 "Mon,"
2357        WHITESPACE@112..113 " "
2358        TEXT@113..115 "04"
2359        WHITESPACE@115..116 " "
2360        TEXT@116..119 "Sep"
2361        WHITESPACE@119..120 " "
2362        TEXT@120..124 "2023"
2363        WHITESPACE@124..125 " "
2364        TEXT@125..133 "18:13:45"
2365        WHITESPACE@133..134 " "
2366        TEXT@134..139 "-0500"
2367      NEWLINE@139..140 "\n"
2368  EMPTY_LINE@140..141
2369    NEWLINE@140..141 "\n"
2370  ENTRY@141..376
2371    ENTRY_HEADER@141..183
2372      IDENTIFIER@141..147 "breezy"
2373      WHITESPACE@147..148 " "
2374      VERSION@148..157 "(3.3.3-2)"
2375      WHITESPACE@157..158 " "
2376      DISTRIBUTIONS@158..166
2377        IDENTIFIER@158..166 "unstable"
2378      METADATA@166..182
2379        SEMICOLON@166..167 ";"
2380        WHITESPACE@167..168 " "
2381        METADATA_ENTRY@168..182
2382          METADATA_KEY@168..175
2383            IDENTIFIER@168..175 "urgency"
2384          EQUALS@175..176 "="
2385          METADATA_VALUE@176..182
2386            IDENTIFIER@176..182 "medium"
2387      NEWLINE@182..183 "\n"
2388    EMPTY_LINE@183..184
2389      NEWLINE@183..184 "\n"
2390    ENTRY_BODY@184..249
2391      INDENT@184..186 "  "
2392      DETAIL@186..248 "* Drop unnecessary de ..."
2393      NEWLINE@248..249 "\n"
2394    ENTRY_BODY@249..302
2395      INDENT@249..251 "  "
2396      DETAIL@251..301 "* Drop dependency on  ..."
2397      NEWLINE@301..302 "\n"
2398    EMPTY_LINE@302..303
2399      NEWLINE@302..303 "\n"
2400    ENTRY_FOOTER@303..376
2401      INDENT@303..307 " -- "
2402      MAINTAINER@307..322
2403        TEXT@307..313 "Jelmer"
2404        WHITESPACE@313..314 " "
2405        TEXT@314..322 "Vernooij"
2406      WHITESPACE@322..323 " "
2407      EMAIL@323..342 "<jelmer@debian.org>"
2408      WHITESPACE@342..344 "  "
2409      TIMESTAMP@344..375
2410        TEXT@344..348 "Sat,"
2411        WHITESPACE@348..349 " "
2412        TEXT@349..351 "24"
2413        WHITESPACE@351..352 " "
2414        TEXT@352..355 "Jun"
2415        WHITESPACE@355..356 " "
2416        TEXT@356..360 "2023"
2417        WHITESPACE@360..361 " "
2418        TEXT@361..369 "14:58:57"
2419        WHITESPACE@369..370 " "
2420        TEXT@370..375 "+0100"
2421      NEWLINE@375..376 "\n"
2422  EMPTY_LINE@376..377
2423    NEWLINE@376..377 "\n"
2424  COMMENT@377..405 "# Oh, and here is a c ..."
2425"###
2426        );
2427
2428        let mut root = parsed.tree_mut();
2429        let entries: Vec<_> = root.iter().collect();
2430        assert_eq!(entries.len(), 2);
2431        let entry = &entries[0];
2432        assert_eq!(entry.package(), Some("breezy".into()));
2433        assert_eq!(entry.version(), Some("3.3.4-1".parse().unwrap()));
2434        assert_eq!(entry.distributions(), Some(vec!["unstable".into()]));
2435        assert_eq!(entry.urgency(), Some(Urgency::Low));
2436        assert_eq!(entry.maintainer(), Some("Jelmer Vernooij".into()));
2437        assert_eq!(entry.email(), Some("jelmer@debian.org".into()));
2438        assert_eq!(
2439            entry.timestamp(),
2440            Some("Mon, 04 Sep 2023 18:13:45 -0500".into())
2441        );
2442        #[cfg(feature = "chrono")]
2443        assert_eq!(
2444            entry.datetime(),
2445            Some("2023-09-04T18:13:45-05:00".parse().unwrap())
2446        );
2447        let changes_lines: Vec<_> = entry.change_lines().collect();
2448        assert_eq!(changes_lines, vec!["* New upstream release.".to_string()]);
2449
2450        assert_eq!(node.text(), CHANGELOG);
2451
2452        let first = root.pop_first().unwrap();
2453        assert_eq!(first.version(), Some("3.3.4-1".parse().unwrap()));
2454        assert_eq!(
2455            root.to_string(),
2456            r#"breezy (3.3.3-2) unstable; urgency=medium
2457
2458  * Drop unnecessary dependency on python3-six. Closes: #1039011
2459  * Drop dependency on cython3-dbg. Closes: #1040544
2460
2461 -- Jelmer Vernooij <jelmer@debian.org>  Sat, 24 Jun 2023 14:58:57 +0100
2462
2463# Oh, and here is a comment
2464"#
2465        );
2466    }
2467
2468    #[test]
2469    fn test_from_io_read() {
2470        let changelog = r#"breezy (3.3.4-1) unstable; urgency=low
2471
2472  * New upstream release.
2473
2474 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2475"#;
2476
2477        let input = changelog.as_bytes();
2478        let input = Box::new(std::io::Cursor::new(input)) as Box<dyn std::io::Read>;
2479        let parsed = ChangeLog::read(input).unwrap();
2480        assert_eq!(parsed.to_string(), changelog);
2481    }
2482
2483    #[test]
2484    #[cfg(feature = "chrono")]
2485    fn test_new_entry() {
2486        let mut cl = ChangeLog::new();
2487        cl.new_entry()
2488            .package("breezy".into())
2489            .version("3.3.4-1".parse().unwrap())
2490            .distributions(vec!["unstable".into()])
2491            .urgency(Urgency::Low)
2492            .maintainer(("Jelmer Vernooij".into(), "jelmer@debian.org".into()))
2493            .change_line("* A change.".into())
2494            .datetime("Mon, 04 Sep 2023 18:13:45 -0500")
2495            .finish();
2496        assert_eq!(
2497            r###"breezy (3.3.4-1) unstable; urgency=low
2498
2499  * A change.
2500
2501 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2502"###,
2503            cl.to_string()
2504        );
2505
2506        assert!(!cl.iter().next().unwrap().is_unreleased().unwrap());
2507    }
2508
2509    #[test]
2510    #[cfg(feature = "chrono")]
2511    fn test_new_empty_default() {
2512        let mut cl = ChangeLog::new();
2513        cl.new_entry()
2514            .package("breezy".into())
2515            .version("3.3.4-1".parse().unwrap())
2516            .maintainer(("Jelmer Vernooij".into(), "jelmer@debian.org".into()))
2517            .change_line("* A change.".into())
2518            .datetime("Mon, 04 Sep 2023 18:13:45 -0500")
2519            .finish();
2520        assert_eq!(
2521            r###"breezy (3.3.4-1) UNRELEASED; urgency=low
2522
2523  * A change.
2524
2525 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2526"###,
2527            cl.to_string()
2528        );
2529    }
2530
2531    #[test]
2532    fn test_new_empty_entry() {
2533        let mut cl = ChangeLog::new();
2534        cl.new_empty_entry()
2535            .change_line("* A change.".into())
2536            .finish();
2537        assert_eq!(
2538            r###"
2539
2540  * A change.
2541
2542 -- 
2543"###,
2544            cl.to_string()
2545        );
2546        assert_eq!(cl.iter().next().unwrap().is_unreleased(), Some(true));
2547    }
2548
2549    #[test]
2550    fn test_parse_invalid_line() {
2551        let text = r#"THIS IS NOT A PARSEABLE LINE
2552lintian-brush (0.35) UNRELEASED; urgency=medium
2553
2554  * Support updating templated debian/control files that use cdbs
2555    template.
2556
2557 -- Joe Example <joe@example.com>  Fri, 04 Oct 2019 02:36:13 +0000
2558"#;
2559        let cl = ChangeLog::read_relaxed(text.as_bytes()).unwrap();
2560        let entry = cl.iter().nth(1).unwrap();
2561        assert_eq!(entry.package(), Some("lintian-brush".into()));
2562        assert_eq!(entry.version(), Some("0.35".parse().unwrap()));
2563        assert_eq!(entry.urgency(), Some(Urgency::Medium));
2564        assert_eq!(entry.maintainer(), Some("Joe Example".into()));
2565        assert_eq!(entry.email(), Some("joe@example.com".into()));
2566        assert_eq!(entry.distributions(), Some(vec!["UNRELEASED".into()]));
2567        #[cfg(feature = "chrono")]
2568        assert_eq!(
2569            entry.datetime(),
2570            Some("2019-10-04T02:36:13+00:00".parse().unwrap())
2571        );
2572    }
2573
2574    #[cfg(test)]
2575    mod entry_manipulate_tests {
2576        use super::*;
2577
2578        #[test]
2579        fn test_append_change_line() {
2580            let mut cl = ChangeLog::new();
2581
2582            let entry = cl
2583                .new_empty_entry()
2584                .change_line("* A change.".into())
2585                .finish();
2586
2587            entry.append_change_line("* Another change.");
2588
2589            assert_eq!(
2590                r###"
2591
2592  * A change.
2593  * Another change.
2594
2595 -- 
2596"###,
2597                cl.to_string()
2598            );
2599        }
2600
2601        #[test]
2602        fn test_prepend_change_line() {
2603            let mut cl = ChangeLog::new();
2604
2605            let entry = cl
2606                .new_empty_entry()
2607                .change_line("* A change.".into())
2608                .finish();
2609
2610            entry.prepend_change_line("* Another change.");
2611
2612            assert_eq!(
2613                r###"
2614
2615  * Another change.
2616  * A change.
2617
2618 -- 
2619"###,
2620                cl.to_string()
2621            );
2622
2623            assert_eq!(entry.maintainer(), None);
2624            assert_eq!(entry.email(), None);
2625            assert_eq!(entry.timestamp(), None);
2626            assert_eq!(entry.package(), None);
2627            assert_eq!(entry.version(), None);
2628        }
2629    }
2630
2631    #[cfg(test)]
2632    mod auto_add_change_tests {
2633        #[test]
2634        fn test_unreleased_existing() {
2635            let text = r#"lintian-brush (0.35) unstable; urgency=medium
2636
2637  * This line already existed.
2638
2639  [ Jane Example ]
2640  * And this one has an existing author.
2641
2642 -- 
2643"#;
2644            let mut cl = super::ChangeLog::read(text.as_bytes()).unwrap();
2645
2646            let entry = cl.iter().next().unwrap();
2647            assert_eq!(entry.package(), Some("lintian-brush".into()));
2648            assert_eq!(entry.is_unreleased(), Some(true));
2649
2650            let entry = cl
2651                .try_auto_add_change(
2652                    &["* And this one is new."],
2653                    ("Joe Example".to_string(), "joe@example.com".to_string()),
2654                    None::<String>,
2655                    None,
2656                )
2657                .unwrap();
2658
2659            assert_eq!(cl.iter().count(), 1);
2660
2661            assert_eq!(entry.package(), Some("lintian-brush".into()));
2662            assert_eq!(entry.is_unreleased(), Some(true));
2663            assert_eq!(
2664                entry.change_lines().collect::<Vec<_>>(),
2665                &[
2666                    "* This line already existed.",
2667                    "",
2668                    "[ Jane Example ]",
2669                    "* And this one has an existing author.",
2670                    "",
2671                    "[ Joe Example ]",
2672                    "* And this one is new.",
2673                ]
2674            );
2675        }
2676    }
2677
2678    #[test]
2679    fn test_ensure_first_line() {
2680        let text = r#"lintian-brush (0.35) unstable; urgency=medium
2681
2682  * This line already existed.
2683
2684  [ Jane Example ]
2685  * And this one has an existing author.
2686
2687 -- 
2688"#;
2689        let cl = ChangeLog::read(text.as_bytes()).unwrap();
2690
2691        let entry = cl.iter().next().unwrap();
2692        assert_eq!(entry.package(), Some("lintian-brush".into()));
2693
2694        entry.ensure_first_line("* QA upload.");
2695        entry.ensure_first_line("* QA upload.");
2696
2697        assert_eq!(
2698            r#"lintian-brush (0.35) unstable; urgency=medium
2699
2700  * QA upload.
2701  * This line already existed.
2702
2703  [ Jane Example ]
2704  * And this one has an existing author.
2705
2706 -- 
2707"#,
2708            cl.to_string()
2709        );
2710    }
2711
2712    #[test]
2713    fn test_set_version() {
2714        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2715
2716  * New upstream release.
2717
2718 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2719"#
2720        .parse()
2721        .unwrap();
2722
2723        entry.set_version(&"3.3.5-1".parse().unwrap());
2724
2725        assert_eq!(
2726            r#"breezy (3.3.5-1) unstable; urgency=low
2727
2728  * New upstream release.
2729
2730 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2731"#,
2732            entry.to_string()
2733        );
2734    }
2735
2736    #[test]
2737    fn test_set_package() {
2738        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2739
2740  * New upstream release.
2741
2742 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2743"#
2744        .parse()
2745        .unwrap();
2746
2747        entry.set_package("bzr".into());
2748
2749        assert_eq!(
2750            r#"bzr (3.3.4-1) unstable; urgency=low
2751
2752  * New upstream release.
2753
2754 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2755"#,
2756            entry.to_string()
2757        );
2758    }
2759
2760    #[test]
2761    fn test_set_distributions() {
2762        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2763
2764  * New upstream release.
2765
2766 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2767"#
2768        .parse()
2769        .unwrap();
2770
2771        entry.set_distributions(vec!["unstable".into(), "experimental".into()]);
2772
2773        assert_eq!(
2774            r#"breezy (3.3.4-1) unstable experimental; urgency=low
2775
2776  * New upstream release.
2777
2778 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2779"#,
2780            entry.to_string()
2781        );
2782    }
2783
2784    #[test]
2785    fn test_set_distributions_no_existing() {
2786        let mut entry: Entry = r#"breezy (3.3.4-1); urgency=low
2787
2788  * New upstream release.
2789
2790 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2791"#
2792        .parse()
2793        .unwrap();
2794
2795        entry.set_distributions(vec!["unstable".into()]);
2796
2797        assert!(entry.to_string().contains("unstable"));
2798    }
2799
2800    #[test]
2801    fn test_set_maintainer() {
2802        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2803
2804  * New upstream release.
2805
2806 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2807"#
2808        .parse()
2809        .unwrap();
2810
2811        entry.set_maintainer(("Joe Example".into(), "joe@example.com".into()));
2812
2813        assert_eq!(
2814            r#"breezy (3.3.4-1) unstable; urgency=low
2815
2816  * New upstream release.
2817
2818 -- Joe Example <joe@example.com>  Mon, 04 Sep 2023 18:13:45 -0500
2819"#,
2820            entry.to_string()
2821        );
2822    }
2823
2824    #[test]
2825    fn test_set_timestamp() {
2826        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2827
2828  * New upstream release.
2829
2830 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2831"#
2832        .parse()
2833        .unwrap();
2834
2835        entry.set_timestamp("Mon, 04 Sep 2023 18:13:46 -0500".into());
2836
2837        assert_eq!(
2838            r#"breezy (3.3.4-1) unstable; urgency=low
2839
2840  * New upstream release.
2841
2842 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:46 -0500
2843"#,
2844            entry.to_string()
2845        );
2846    }
2847
2848    #[test]
2849    #[cfg(feature = "chrono")]
2850    fn test_set_datetime() {
2851        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2852
2853  * New upstream release.
2854
2855 -- Jelmer Vernooij <joe@example.com>  Mon, 04 Sep 2023 18:13:45 -0500
2856"#
2857        .parse()
2858        .unwrap();
2859
2860        entry.set_datetime("2023-09-04T18:13:46-05:00".parse().unwrap());
2861
2862        assert_eq!(
2863            r#"breezy (3.3.4-1) unstable; urgency=low
2864
2865  * New upstream release.
2866
2867 -- Jelmer Vernooij <joe@example.com>  Mon, 04 Sep 2023 18:13:46 -0500
2868"#,
2869            entry.to_string()
2870        );
2871    }
2872
2873    #[test]
2874    fn test_set_urgency() {
2875        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2876
2877  * New upstream release.
2878
2879 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2880"#
2881        .parse()
2882        .unwrap();
2883
2884        entry.set_urgency(Urgency::Medium);
2885
2886        assert_eq!(
2887            r#"breezy (3.3.4-1) unstable; urgency=medium
2888
2889  * New upstream release.
2890
2891 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2892"#,
2893            entry.to_string()
2894        );
2895    }
2896
2897    #[test]
2898    fn test_set_metadata() {
2899        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2900
2901  * New upstream release.
2902
2903 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2904"#
2905        .parse()
2906        .unwrap();
2907
2908        entry.set_metadata("foo", "bar");
2909
2910        assert_eq!(
2911            r#"breezy (3.3.4-1) unstable; urgency=low foo=bar
2912
2913  * New upstream release.
2914
2915 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2916"#,
2917            entry.to_string()
2918        );
2919    }
2920
2921    #[test]
2922    fn test_set_metadata_replace_existing() {
2923        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low foo=old
2924
2925  * New upstream release.
2926
2927 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2928"#
2929        .parse()
2930        .unwrap();
2931
2932        entry.set_metadata("foo", "new");
2933
2934        assert_eq!(
2935            r#"breezy (3.3.4-1) unstable; urgency=low foo=new
2936
2937  * New upstream release.
2938
2939 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2940"#,
2941            entry.to_string()
2942        );
2943    }
2944
2945    #[test]
2946    fn test_set_metadata_after_distributions() {
2947        let mut entry: Entry = r#"breezy (3.3.4-1) unstable experimental; urgency=low
2948
2949  * New upstream release.
2950
2951 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2952"#
2953        .parse()
2954        .unwrap();
2955
2956        entry.set_metadata("foo", "bar");
2957
2958        assert_eq!(
2959            r#"breezy (3.3.4-1) unstable experimental; urgency=low foo=bar
2960
2961  * New upstream release.
2962
2963 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
2964"#,
2965            entry.to_string()
2966        );
2967    }
2968
2969    #[test]
2970    fn test_add_change_for_author() {
2971        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
2972
2973  * New upstream release.
2974
2975  [ Jelmer Vernooij ]
2976  * A change by the maintainer.
2977
2978 -- Joe Example <joe@example.com>  Mon, 04 Sep 2023 18:13:45 -0500
2979"#
2980        .parse()
2981        .unwrap();
2982
2983        entry
2984            .try_add_change_for_author(
2985                &["A change by the maintainer."],
2986                ("Jelmer Vernooij".into(), "jelmer@debian.org".into()),
2987            )
2988            .unwrap();
2989    }
2990
2991    #[test]
2992    fn test_changelog_from_entry_iter() {
2993        let text = r#"breezy (3.3.4-1) unstable; urgency=low
2994
2995  * New upstream release.
2996
2997 -- Jelmer Vernooij <jelmer@jelmer.uk>  Mon, 04 Sep 2023 18:13:45 -0500
2998"#;
2999
3000        let entry: Entry = text.parse().unwrap();
3001
3002        let cl = std::iter::once(entry).collect::<ChangeLog>();
3003
3004        assert_eq!(cl.to_string(), text);
3005    }
3006
3007    #[test]
3008    fn test_pop_change_line() {
3009        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3010
3011  * New upstream release.
3012  * Fixed bug #123.
3013  * Added new feature.
3014
3015 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3016"#
3017        .parse()
3018        .unwrap();
3019
3020        // Test popping existing lines
3021        assert_eq!(
3022            entry.pop_change_line(),
3023            Some("* Added new feature.".to_string())
3024        );
3025        assert_eq!(
3026            entry.pop_change_line(),
3027            Some("* Fixed bug #123.".to_string())
3028        );
3029        assert_eq!(
3030            entry.pop_change_line(),
3031            Some("* New upstream release.".to_string())
3032        );
3033
3034        // Test popping from empty entry
3035        assert_eq!(entry.pop_change_line(), None);
3036    }
3037
3038    #[test]
3039    fn test_pop_change_line_empty_entry() {
3040        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3041
3042 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3043"#
3044        .parse()
3045        .unwrap();
3046
3047        assert_eq!(entry.pop_change_line(), None);
3048    }
3049
3050    #[test]
3051    fn test_pop_change_line_empty_string() {
3052        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3053
3054  * Something
3055
3056 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3057"#
3058        .parse()
3059        .unwrap();
3060
3061        entry.pop_change_line();
3062        entry.append_change_line("");
3063        // Empty lines don't have DETAIL tokens, so pop_change_line returns None
3064        assert_eq!(entry.pop_change_line(), None);
3065    }
3066
3067    #[test]
3068    fn test_append_change_line() {
3069        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3070
3071  * New upstream release.
3072
3073 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3074"#
3075        .parse()
3076        .unwrap();
3077
3078        entry.append_change_line("* Fixed bug #456.");
3079
3080        assert_eq!(
3081            entry.to_string(),
3082            r#"breezy (3.3.4-1) unstable; urgency=low
3083
3084  * New upstream release.
3085  * Fixed bug #456.
3086
3087 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3088"#
3089        );
3090    }
3091
3092    #[test]
3093    fn test_append_change_line_empty() {
3094        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3095
3096  * New upstream release.
3097
3098 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3099"#
3100        .parse()
3101        .unwrap();
3102
3103        entry.append_change_line("");
3104
3105        let lines: Vec<String> = entry.change_lines().collect();
3106        // Empty lines are not returned by change_lines()
3107        assert_eq!(lines.len(), 1);
3108        assert_eq!(lines[0], "* New upstream release.".to_string());
3109    }
3110
3111    #[test]
3112    fn test_changelog_write_to_path() {
3113        use tempfile::NamedTempFile;
3114
3115        let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
3116
3117  * New upstream release.
3118
3119 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3120"#
3121        .parse()
3122        .unwrap();
3123
3124        let temp_file = NamedTempFile::new().unwrap();
3125        let path = temp_file.path().to_path_buf();
3126
3127        changelog.write_to_path(&path).unwrap();
3128
3129        let contents = std::fs::read_to_string(&path).unwrap();
3130        assert_eq!(contents, changelog.to_string());
3131    }
3132
3133    #[test]
3134    fn test_changelog_into_iter() {
3135        let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
3136
3137  * New upstream release.
3138
3139 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3140
3141breezy (3.3.3-1) unstable; urgency=low
3142
3143  * Previous release.
3144
3145 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 03 Sep 2023 18:13:45 -0500
3146"#
3147        .parse()
3148        .unwrap();
3149
3150        let entries: Vec<Entry> = changelog.into_iter().collect();
3151        assert_eq!(entries.len(), 2);
3152    }
3153
3154    #[test]
3155    fn test_set_version_no_existing() {
3156        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3157
3158  * New upstream release.
3159
3160 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3161"#
3162        .parse()
3163        .unwrap();
3164
3165        entry.set_version(&"1.0.0".parse().unwrap());
3166
3167        assert!(entry.to_string().contains("(1.0.0)"));
3168    }
3169
3170    #[test]
3171    fn test_entry_footer_set_email_edge_cases() {
3172        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3173
3174  * New upstream release.
3175
3176 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3177"#
3178        .parse()
3179        .unwrap();
3180
3181        // Test checking email through entry
3182        assert_eq!(entry.email(), Some("jelmer@debian.org".to_string()));
3183    }
3184
3185    #[test]
3186    fn test_entry_footer_set_maintainer_edge_cases() {
3187        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3188
3189  * New upstream release.
3190
3191 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3192"#
3193        .parse()
3194        .unwrap();
3195
3196        // Test setting maintainer
3197        entry.set_maintainer(("New Maintainer".into(), "new@example.com".into()));
3198
3199        assert!(entry
3200            .to_string()
3201            .contains("New Maintainer <new@example.com>"));
3202    }
3203
3204    #[test]
3205    fn test_entry_footer_set_timestamp_edge_cases() {
3206        let mut entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3207
3208  * New upstream release.
3209
3210 -- Jelmer Vernooij <jelmer@debian.org>  
3211"#
3212        .parse()
3213        .unwrap();
3214
3215        // Test setting timestamp when it's missing
3216        entry.set_timestamp("Mon, 04 Sep 2023 18:13:45 -0500".into());
3217
3218        assert!(entry
3219            .to_string()
3220            .contains("Mon, 04 Sep 2023 18:13:45 -0500"));
3221    }
3222
3223    #[test]
3224    fn test_parse_multiple_distributions_frozen_unstable() {
3225        // Test case for https://github.com/jelmer/debian-changelog-rs/issues/93
3226        // The "at" package has entries with "frozen unstable" distributions from 1998
3227        const CHANGELOG: &str = r#"at (3.1.8-10) frozen unstable; urgency=high
3228
3229  * Suidunregister /usr/bin (closes: Bug#59421).
3230
3231 -- Siggy Brentrup <bsb@winnegan.de>  Mon,  3 Apr 2000 13:56:47 +0200
3232"#;
3233
3234        let parsed = parse(CHANGELOG);
3235        assert_eq!(parsed.errors(), &Vec::<String>::new());
3236
3237        let root = parsed.tree();
3238        let entries: Vec<_> = root.iter().collect();
3239        assert_eq!(entries.len(), 1);
3240
3241        let entry = &entries[0];
3242        assert_eq!(entry.package(), Some("at".into()));
3243        assert_eq!(entry.version(), Some("3.1.8-10".parse().unwrap()));
3244        assert_eq!(
3245            entry.distributions(),
3246            Some(vec!["frozen".into(), "unstable".into()])
3247        );
3248    }
3249
3250    #[test]
3251    fn test_parse_old_metadata_format_with_comma() {
3252        // Test case for https://github.com/jelmer/debian-changelog-rs/issues/93
3253        // The "at" package has old-style metadata with comma-separated values
3254        const CHANGELOG: &str = r#"at (3.1.8-9) frozen unstable; urgency=low, closes=53715 56047 56607 55560 55514
3255
3256  * Added SIGCHLD handler to release zombies (closes 53715 56047 56607)
3257
3258 -- Siggy Brentrup <bsb@winnegan.de>  Sun, 30 Jan 2000 22:00:46 +0100
3259"#;
3260
3261        let parsed = parse(CHANGELOG);
3262
3263        // This old format currently fails to parse
3264        if !parsed.errors().is_empty() {
3265            eprintln!("Parse errors: {:?}", parsed.errors());
3266        }
3267        assert_eq!(parsed.errors(), &Vec::<String>::new());
3268
3269        let root = parsed.tree();
3270        let entries: Vec<_> = root.iter().collect();
3271        assert_eq!(entries.len(), 1);
3272
3273        let entry = &entries[0];
3274        assert_eq!(entry.package(), Some("at".into()));
3275        assert_eq!(entry.version(), Some("3.1.8-9".parse().unwrap()));
3276        assert_eq!(
3277            entry.distributions(),
3278            Some(vec!["frozen".into(), "unstable".into()])
3279        );
3280        assert_eq!(entry.urgency(), Some(Urgency::Low));
3281
3282        // Verify we can access the "closes" metadata
3283        let header = entry.header().unwrap();
3284        let metadata: Vec<(String, String)> = header.metadata().collect();
3285
3286        // Should have both urgency and closes
3287        assert_eq!(metadata.len(), 2);
3288        assert!(metadata.iter().any(|(k, v)| k == "urgency" && v == "low"));
3289
3290        // Get the closes value and verify exact match
3291        let closes_value = metadata
3292            .iter()
3293            .find(|(k, _)| k == "closes")
3294            .map(|(_, v)| v)
3295            .expect("closes metadata should exist");
3296
3297        assert_eq!(closes_value, "53715 56047 56607 55560 55514");
3298    }
3299
3300    #[test]
3301    fn test_entry_iter_changes_by_author() {
3302        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3303
3304  [ Author 1 ]
3305  * Change by Author 1
3306
3307  [ Author 2 ]  
3308  * Change by Author 2
3309  * Another change by Author 2
3310
3311  * Unattributed change
3312
3313 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3314"#
3315        .parse()
3316        .unwrap();
3317
3318        let changes = entry.iter_changes_by_author();
3319
3320        assert_eq!(changes.len(), 3);
3321
3322        assert_eq!(changes[0].0, Some("Author 1".to_string()));
3323        assert_eq!(changes[0].2, vec!["* Change by Author 1".to_string()]);
3324
3325        assert_eq!(changes[1].0, Some("Author 2".to_string()));
3326        assert_eq!(
3327            changes[1].2,
3328            vec![
3329                "* Change by Author 2".to_string(),
3330                "* Another change by Author 2".to_string()
3331            ]
3332        );
3333
3334        assert_eq!(changes[2].0, None);
3335        assert_eq!(changes[2].2, vec!["* Unattributed change".to_string()]);
3336    }
3337
3338    #[test]
3339    fn test_entry_get_authors() {
3340        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3341
3342  [ Author 1 ]
3343  * Change by Author 1
3344
3345  [ Author 2 ]  
3346  * Change by Author 2
3347
3348 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3349"#
3350        .parse()
3351        .unwrap();
3352
3353        let authors = entry.get_authors();
3354
3355        assert_eq!(authors.len(), 2);
3356        assert!(authors.contains("Author 1"));
3357        assert!(authors.contains("Author 2"));
3358        // Maintainer should not be in the authors from change sections
3359        assert!(!authors.contains("Jelmer Vernooij"));
3360    }
3361
3362    #[test]
3363    fn test_entry_get_maintainer_identity() {
3364        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3365
3366  * New upstream release.
3367
3368 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3369"#
3370        .parse()
3371        .unwrap();
3372
3373        let identity = entry.get_maintainer_identity().unwrap();
3374
3375        assert_eq!(identity.name, "Jelmer Vernooij");
3376        assert_eq!(identity.email, "jelmer@debian.org");
3377    }
3378
3379    #[test]
3380    fn test_entry_get_maintainer_identity_missing() {
3381        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3382
3383  * New upstream release.
3384
3385"#
3386        .parse()
3387        .unwrap();
3388
3389        let identity = entry.get_maintainer_identity();
3390
3391        assert!(identity.is_none());
3392    }
3393
3394    #[test]
3395    fn test_changelog_iter_by_author() {
3396        let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
3397
3398  * New upstream release.
3399
3400 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3401
3402breezy (3.3.3-1) unstable; urgency=low
3403
3404  * Bug fix release.
3405
3406 -- Jane Doe <jane@example.com>  Sun, 03 Sep 2023 17:12:30 -0500
3407
3408breezy (3.3.2-1) unstable; urgency=low
3409
3410  * Another release.
3411
3412 -- Jelmer Vernooij <jelmer@debian.org>  Sat, 02 Sep 2023 16:11:15 -0500
3413"#
3414        .parse()
3415        .unwrap();
3416
3417        let authors: Vec<(String, String, Vec<Entry>)> = changelog.iter_by_author().collect();
3418
3419        assert_eq!(authors.len(), 2);
3420        assert_eq!(authors[0].0, "Jane Doe");
3421        assert_eq!(authors[0].1, "jane@example.com");
3422        assert_eq!(authors[0].2.len(), 1);
3423        assert_eq!(authors[1].0, "Jelmer Vernooij");
3424        assert_eq!(authors[1].1, "jelmer@debian.org");
3425        assert_eq!(authors[1].2.len(), 2);
3426    }
3427
3428    #[test]
3429    fn test_changelog_get_all_authors() {
3430        let changelog: ChangeLog = r#"breezy (3.3.4-1) unstable; urgency=low
3431
3432  [ Contributor 1 ]
3433  * Contribution
3434
3435  * Main change
3436
3437 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3438
3439breezy (3.3.3-1) unstable; urgency=low
3440
3441  * Bug fix release.
3442
3443 -- Jane Doe <jane@example.com>  Sun, 03 Sep 2023 17:12:30 -0500
3444"#
3445        .parse()
3446        .unwrap();
3447
3448        let authors = changelog.get_all_authors();
3449
3450        assert_eq!(authors.len(), 3);
3451
3452        let author_names: std::collections::HashSet<String> = authors
3453            .iter()
3454            .map(|identity| identity.name.clone())
3455            .collect();
3456
3457        assert!(author_names.contains("Jelmer Vernooij"));
3458        assert!(author_names.contains("Jane Doe"));
3459        assert!(author_names.contains("Contributor 1"));
3460    }
3461
3462    #[test]
3463    fn test_add_changes_for_author_no_existing_sections() {
3464        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3465
3466  * Existing change
3467
3468 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 Sep 2023 18:13:45 -0500
3469"#
3470        .parse()
3471        .unwrap();
3472
3473        entry
3474            .try_add_changes_for_author("Alice", vec!["* Alice's change"])
3475            .unwrap();
3476
3477        let lines: Vec<_> = entry.change_lines().collect();
3478
3479        // Should have wrapped existing changes in maintainer's section
3480        assert!(lines.iter().any(|l| l.contains("[ Jelmer Vernooij ]")));
3481        // Should have added Alice's section
3482        assert!(lines.iter().any(|l| l.contains("[ Alice ]")));
3483        // Should have both changes
3484        assert!(lines.iter().any(|l| l.contains("Existing change")));
3485        assert!(lines.iter().any(|l| l.contains("Alice's change")));
3486    }
3487
3488    #[test]
3489    fn test_add_changes_for_author_with_existing_sections() {
3490        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3491
3492  [ Author 1 ]
3493  * Change by Author 1
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 new change"])
3502            .unwrap();
3503
3504        let lines: Vec<_> = entry.change_lines().collect();
3505
3506        // Should have Author 1's section
3507        assert!(lines.iter().any(|l| l.contains("[ Author 1 ]")));
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("Change by Author 1")));
3512        assert!(lines.iter().any(|l| l.contains("Alice's new change")));
3513    }
3514
3515    #[test]
3516    fn test_add_changes_for_author_same_author() {
3517        let entry: Entry = r#"breezy (3.3.4-1) unstable; urgency=low
3518
3519  [ Alice ]
3520  * First change
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!["* Second change"])
3529            .unwrap();
3530
3531        let lines: Vec<_> = entry.change_lines().collect();
3532
3533        // Should only have one Alice section (not duplicated)
3534        let alice_count = lines.iter().filter(|l| l.contains("[ Alice ]")).count();
3535        assert_eq!(alice_count, 1);
3536
3537        // Should have both changes
3538        assert!(lines.iter().any(|l| l.contains("First change")));
3539        assert!(lines.iter().any(|l| l.contains("Second change")));
3540    }
3541
3542    #[test]
3543    fn test_datetime_with_incorrect_day_of_week() {
3544        // Test for bug: datetime() should parse leniently even when day-of-week doesn't match
3545        // This changelog entry has "Mon, 22 Mar 2011" but Mar 22, 2011 was actually Tuesday
3546        let entry: Entry = r#"blah (0.1-2) UNRELEASED; urgency=medium
3547
3548  * New release.
3549
3550 -- Jelmer Vernooij <jelmer@debian.org>  Mon, 22 Mar 2011 16:47:42 +0000
3551"#
3552        .parse()
3553        .unwrap();
3554
3555        // timestamp() should return just the date portion
3556        assert_eq!(
3557            entry.timestamp(),
3558            Some("Mon, 22 Mar 2011 16:47:42 +0000".into())
3559        );
3560
3561        // datetime() should successfully parse the timestamp despite incorrect day-of-week
3562        let datetime = entry.datetime();
3563        assert!(
3564            datetime.is_some(),
3565            "datetime() should not return None for timestamp with incorrect day-of-week"
3566        );
3567        assert_eq!(datetime.unwrap().to_rfc3339(), "2011-03-22T16:47:42+00:00");
3568    }
3569
3570    #[test]
3571    fn test_line_col() {
3572        let text = r#"foo (1.0-1) unstable; urgency=low
3573
3574  * First change
3575
3576 -- Maintainer <email@example.com>  Mon, 01 Jan 2024 12:00:00 +0000
3577
3578bar (2.0-1) experimental; urgency=high
3579
3580  * Second change
3581  * Third change
3582
3583 -- Another <another@example.com>  Tue, 02 Jan 2024 13:00:00 +0000
3584"#;
3585        let changelog = text.parse::<ChangeLog>().unwrap();
3586
3587        // Test changelog root position
3588        assert_eq!(changelog.line(), 0);
3589        assert_eq!(changelog.column(), 0);
3590        assert_eq!(changelog.line_col(), (0, 0));
3591
3592        // Test entry line numbers
3593        let entries: Vec<_> = changelog.iter().collect();
3594        assert_eq!(entries.len(), 2);
3595
3596        // First entry starts at line 0
3597        assert_eq!(entries[0].line(), 0);
3598        assert_eq!(entries[0].column(), 0);
3599        assert_eq!(entries[0].line_col(), (0, 0));
3600
3601        // Second entry starts at line 6 (after first entry and empty line)
3602        assert_eq!(entries[1].line(), 6);
3603        assert_eq!(entries[1].column(), 0);
3604        assert_eq!(entries[1].line_col(), (6, 0));
3605
3606        // Test entry components
3607        let header = entries[0].header().unwrap();
3608        assert_eq!(header.line(), 0);
3609        assert_eq!(header.column(), 0);
3610
3611        let body = entries[0].body().unwrap();
3612        assert_eq!(body.line(), 2); // Body starts at first change line
3613
3614        let footer = entries[0].footer().unwrap();
3615        assert_eq!(footer.line(), 4); // Footer line
3616
3617        // Test maintainer and timestamp nodes
3618        let maintainer = entries[0].maintainer_node().unwrap();
3619        assert_eq!(maintainer.line(), 4); // On footer line
3620
3621        let timestamp = entries[0].timestamp_node().unwrap();
3622        assert_eq!(timestamp.line(), 4); // On footer line
3623
3624        // Verify second entry components
3625        let header2 = entries[1].header().unwrap();
3626        assert_eq!(header2.line(), 6);
3627
3628        let footer2 = entries[1].footer().unwrap();
3629        assert_eq!(footer2.line(), 11);
3630    }
3631}