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