Skip to main content

debian_analyzer/
detect_gbp_dch.rs

1//! Detect whether the changelog should be updated.
2use breezyshim::error::Error;
3use breezyshim::graph::Graph;
4use breezyshim::prelude::*;
5use breezyshim::revisionid::RevisionId;
6use debian_changelog::{ChangeLog, Entry as ChangeLogEntry};
7use lazy_regex::regex;
8
9#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
10/// Behaviour for updating the changelog.
11pub struct ChangelogBehaviour {
12    #[serde(rename = "update")]
13    /// Whether the changelog should be updated.
14    pub update_changelog: bool,
15
16    /// Explanation for the decision.
17    pub explanation: String,
18}
19
20#[cfg(feature = "svp")]
21impl From<ChangelogBehaviour> for svp_client::ChangelogBehaviour {
22    fn from(b: ChangelogBehaviour) -> Self {
23        svp_client::ChangelogBehaviour {
24            update_changelog: b.update_changelog,
25            explanation: b.explanation,
26        }
27    }
28}
29
30impl From<ChangelogBehaviour> for (bool, String) {
31    fn from(b: ChangelogBehaviour) -> Self {
32        (b.update_changelog, b.explanation)
33    }
34}
35
36impl From<&ChangelogBehaviour> for (bool, String) {
37    fn from(b: &ChangelogBehaviour) -> Self {
38        (b.update_changelog, b.explanation.clone())
39    }
40}
41
42// Number of revisions to search back
43const DEFAULT_BACKLOG: usize = 50;
44
45// TODO(jelmer): Check that what's added in the changelog is actually based on
46// what was in the commit messages?
47
48fn gbp_conf_has_dch_section(tree: &dyn Tree, debian_path: &std::path::Path) -> bool {
49    let gbp_conf_path = debian_path.join("gbp.conf");
50    let gbp_conf_text = match tree.get_file_text(gbp_conf_path.as_path()) {
51        Ok(text) => text,
52        Err(Error::NoSuchFile(_)) => return false,
53        Err(e) => panic!("Unexpected error reading gbp.conf: {:?}", e),
54    };
55
56    let mut parser = configparser::ini::Ini::new();
57    parser
58        .read(String::from_utf8_lossy(gbp_conf_text.as_slice()).to_string())
59        .unwrap();
60    parser.sections().contains(&"dch".to_string())
61}
62
63/// Guess whether the changelog should be updated.
64///
65/// # Arguments
66/// * `tree` - Tree to edit
67/// * `debian_path` - Path to packaging in tree
68///
69/// # Returns
70/// * `None` if it is not possible to guess
71/// * `True` if the changelog should be updated
72/// * `False` if the changelog should not be updated
73pub fn guess_update_changelog(
74    tree: &dyn WorkingTree,
75    debian_path: &std::path::Path,
76    mut cl: Option<ChangeLog>,
77) -> Option<ChangelogBehaviour> {
78    if debian_path != std::path::Path::new("debian") {
79        return Some(ChangelogBehaviour{
80            update_changelog: true,
81            explanation: "assuming changelog needs to be updated since gbp dch only supports a debian directory in the root of the repository".to_string(),
82        });
83    }
84    let changelog_path = debian_path.join("changelog");
85    if cl.is_none() {
86        match tree.get_file(changelog_path.as_path()) {
87            Ok(f) => {
88                cl = Some(ChangeLog::read(f).unwrap());
89            }
90            Err(Error::NoSuchFile(_)) => {
91                log::debug!("No changelog found");
92            }
93            Err(e) => {
94                panic!("Unexpected error reading changelog: {:?}", e);
95            }
96        }
97    }
98    if let Some(ref cl) = cl {
99        if debian_changelog::is_unreleased_inaugural(cl) {
100            return Some(ChangelogBehaviour {
101                update_changelog: false,
102                explanation: "assuming changelog does not need to be updated since it is the inaugural unreleased entry".to_string()
103            });
104        }
105        if let Some(first_entry) = cl.iter().next() {
106            for line in first_entry.change_lines() {
107                if line.contains("generated at release time") {
108                    return Some(ChangelogBehaviour {
109                        update_changelog: false,
110                        explanation:
111                            "last changelog entry warns changelog is generated at release time"
112                                .to_string(),
113                    });
114                }
115            }
116        }
117    }
118    if let Some(ret) = guess_update_changelog_from_tree(tree, debian_path, cl) {
119        Some(ret)
120    } else {
121        guess_update_changelog_from_branch(&tree.branch(), debian_path, None)
122    }
123}
124
125/// Guess whether the changelog should be updated by looking at tree contents
126pub fn guess_update_changelog_from_tree(
127    tree: &dyn Tree,
128    debian_path: &std::path::Path,
129    cl: Option<ChangeLog>,
130) -> Option<ChangelogBehaviour> {
131    if gbp_conf_has_dch_section(tree, debian_path) {
132        return Some(ChangelogBehaviour {
133            update_changelog: false,
134            explanation: "Assuming changelog does not need to be updated, since there is a [dch] section in gbp.conf.".to_string()
135        });
136    }
137
138    // TODO(jelmes): Do something more clever here, perhaps looking at history of the changelog file?
139    if let Some(cl) = cl {
140        if let Some(entry) = cl.iter().next() {
141            if all_sha_prefixed(&entry) {
142                return Some(ChangelogBehaviour {
143                    update_changelog: false,
144                    explanation: "Assuming changelog does not need to be updated, since all entries in last changelog entry are prefixed by git shas.".to_string()
145                });
146            }
147        }
148    }
149
150    None
151}
152
153fn greedy_revisions(graph: &Graph, revid: &RevisionId, length: usize) -> (Vec<RevisionId>, bool) {
154    let mut ret = vec![];
155    let mut it = match graph.iter_lefthand_ancestry(revid, None) {
156        Ok(iter) => iter,
157        Err(_) => return (ret, true),
158    };
159    while ret.len() < length {
160        ret.push(match it.next() {
161            None => break,
162            Some(Ok(rev)) => rev,
163            Some(Err(Error::RevisionNotPresent(_))) => {
164                if !ret.is_empty() {
165                    ret.pop();
166                }
167                // Shallow history
168                return (ret, true);
169            }
170            Some(Err(e)) => {
171                // Re-raise other errors
172                panic!("Error iterating through ancestry: {:?}", e);
173            }
174        });
175    }
176    (ret, false)
177}
178
179#[derive(Debug, Default)]
180struct ChangelogStats {
181    mixed: usize,
182    changelog_only: usize,
183    other_only: usize,
184    dch_references: usize,
185    unreleased_references: usize,
186}
187
188fn changelog_stats(
189    branch: &dyn Branch,
190    history: usize,
191    debian_path: &std::path::Path,
192) -> ChangelogStats {
193    let mut ret = ChangelogStats::default();
194    let branch_lock = branch.lock_read();
195    let graph = branch.repository().get_graph();
196    let (revids, _truncated) = greedy_revisions(&graph, &branch.last_revision(), history);
197    let mut revs = vec![];
198    for (_revid, rev) in branch.repository().iter_revisions(revids) {
199        if rev.is_none() {
200            // Ghost
201            continue;
202        }
203        let rev = rev.unwrap();
204        if rev.message.contains("Git-Dch: ") || rev.message.contains("Gbp-Dch: ") {
205            ret.dch_references += 1;
206        }
207        revs.push(rev);
208    }
209    for (rev, delta) in revs.iter().zip(
210        branch
211            .repository()
212            .get_revision_deltas(revs.as_slice(), None),
213    ) {
214        let filenames: Vec<_> = delta
215            .added
216            .iter()
217            .filter_map(|a| a.path.1.as_ref())
218            .chain(delta.removed.iter().filter_map(|r| r.path.0.as_ref()))
219            .chain(delta.renamed.iter().filter_map(|r| r.path.0.as_ref()))
220            .chain(delta.renamed.iter().filter_map(|r| r.path.1.as_ref()))
221            .chain(delta.modified.iter().filter_map(|m| m.path.0.as_ref()))
222            .cloned()
223            .collect();
224        if !filenames.iter().any(|f| f.starts_with(debian_path)) {
225            continue;
226        }
227        let cl_path = debian_path.join("changelog");
228        if filenames.contains(&cl_path) {
229            let revtree = branch.repository().revision_tree(&rev.revision_id).unwrap();
230            match revtree.get_file_lines(cl_path.as_path()) {
231                Err(Error::NoSuchFile(_p)) => {}
232                Err(e) => {
233                    panic!("Error reading changelog: {}", e);
234                }
235                Ok(cl_lines) => {
236                    if String::from_utf8_lossy(cl_lines[0].as_slice()).contains("UNRELEASED") {
237                        ret.unreleased_references += 1;
238                    }
239                }
240            }
241            if filenames.len() > 1 {
242                ret.mixed += 1;
243            } else {
244                ret.changelog_only += 1;
245            }
246        } else {
247            ret.other_only += 1;
248        }
249    }
250    std::mem::drop(branch_lock);
251    ret
252}
253
254/// Guess whether the changelog should be updated manually.
255///
256/// # Arguments
257///
258///  * `branch` - A branch object
259///  * `debian_path` - Path to the debian directory
260///  * `history` - Number of revisions back to analyze
261///
262/// # Returns
263///
264///   boolean indicating whether changelog should be updated
265pub fn guess_update_changelog_from_branch(
266    branch: &dyn Branch,
267    debian_path: &std::path::Path,
268    history: Option<usize>,
269) -> Option<ChangelogBehaviour> {
270    let history = history.unwrap_or(DEFAULT_BACKLOG);
271    // Two indications this branch may be doing changelog entries at
272    // release time:
273    // - "Git-Dch: " or "Gbp-Dch: " is used in the commit messages
274    // - The vast majority of lines in changelog get added in
275    //   commits that only touch the changelog
276    let stats = changelog_stats(branch, history, debian_path);
277    log::debug!("Branch history analysis: changelog_only: {}, other_only: {}, mixed: {}, dch_references: {}, unreleased_references: {}",
278                  stats.changelog_only, stats.other_only, stats.mixed, stats.dch_references,
279                  stats.unreleased_references);
280    if stats.dch_references > 0 {
281        return Some(ChangelogBehaviour {
282            update_changelog: false,
283            explanation: "Assuming changelog does not need to be updated, since there are Gbp-Dch stanzas in commit messages".to_string()
284        });
285    }
286    if stats.changelog_only == 0 {
287        return Some(ChangelogBehaviour {
288            update_changelog: true,
289            explanation: "Assuming changelog needs to be updated, since it is always changed together with other files in the tree.".to_string()
290        });
291    }
292    if stats.unreleased_references == 0 {
293        return Some(ChangelogBehaviour {
294            update_changelog: false,
295            explanation: "Assuming changelog does not need to be updated, since it never uses UNRELEASED entries".to_string()
296        });
297    }
298    if stats.mixed == 0 && stats.changelog_only > 0 && stats.other_only > 0 {
299        // changelog is *always* updated in a separate commit.
300        return Some(ChangelogBehaviour {
301            update_changelog: false,
302            explanation: "Assuming changelog does not need to be updated, since changelog entries are always updated in separate commits.".to_string()
303        });
304    }
305    // Is this a reasonable threshold?
306    if stats.changelog_only > stats.mixed && stats.other_only > stats.mixed {
307        return Some(ChangelogBehaviour{
308            update_changelog: false,
309            explanation: "Assuming changelog does not need to be updated, since changelog entries are usually updated in separate commits.".to_string()
310        });
311    }
312    None
313}
314
315/// This is generally done by gbp-dch(1).
316///
317/// # Arguments
318///
319/// * `cl` - Changelog entry
320pub fn all_sha_prefixed(cb: &ChangeLogEntry) -> bool {
321    let mut sha_prefixed = 0;
322    for change in cb.change_lines() {
323        if !change.starts_with("* ") {
324            continue;
325        }
326        if regex!(r"\* \[[0-9a-f]{7}\] ").is_match(change.as_str()) {
327            sha_prefixed += 1;
328        } else {
329            return false;
330        }
331    }
332
333    sha_prefixed > 0
334}
335
336#[cfg(test)]
337mod tests {
338    use super::*;
339    use breezyshim::controldir::{create_standalone_workingtree, ControlDirFormat};
340    use std::path::Path;
341    pub const COMMITTER: &str = "Test User <test@example.com>";
342    fn make_changelog(entries: Vec<String>) -> String {
343        format!(
344            r###"lintian-brush (0.1) UNRELEASED; urgency=medium
345
346{}
347 -- Jelmer Vernooij <jelmer@debian.org>  Sat, 13 Oct 2018 11:21:39 +0100
348"###,
349            entries
350                .iter()
351                .map(|x| format!("  * {}\n", x))
352                .collect::<Vec<_>>()
353                .concat()
354        )
355    }
356
357    #[test]
358    fn test_no_gbp_conf() {
359        let td = tempfile::tempdir().unwrap();
360        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
361        assert_eq!(
362            Some(ChangelogBehaviour{
363                update_changelog: true,
364                explanation: "Assuming changelog needs to be updated, since it is always changed together with other files in the tree.".to_string(),
365            }),
366            guess_update_changelog(&tree, Path::new("debian"), None),
367        );
368    }
369
370    #[test]
371    fn test_custom_path() {
372        let td = tempfile::tempdir().unwrap();
373        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
374        assert_eq!(
375            Some(ChangelogBehaviour{
376                update_changelog: true,
377                explanation: "Assuming changelog needs to be updated, since it is always changed together with other files in the tree.".to_string(),
378            }),
379            guess_update_changelog(&tree, Path::new("debian"), None),
380        );
381        assert_eq!(
382            Some(ChangelogBehaviour{
383                update_changelog: true,
384                explanation: "assuming changelog needs to be updated since gbp dch only supports a debian directory in the root of the repository".to_string(),
385            }),
386            guess_update_changelog(&tree, Path::new(""), None),
387        );
388        assert_eq!(
389            Some(ChangelogBehaviour{
390                update_changelog: true,
391                explanation: "assuming changelog needs to be updated since gbp dch only supports a debian directory in the root of the repository".to_string(),
392            }),
393            guess_update_changelog(&tree, Path::new("lala/debian"), None),
394        );
395    }
396
397    #[test]
398    fn test_gbp_conf_dch() {
399        let td = tempfile::tempdir().unwrap();
400        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
401        std::fs::create_dir(td.path().join("debian")).unwrap();
402        std::fs::write(
403            td.path().join("debian/gbp.conf"),
404            r#"[dch]
405pristine-tar = False
406"#,
407        )
408        .unwrap();
409        tree.add(&[Path::new("debian"), Path::new("debian/gbp.conf")])
410            .unwrap();
411        assert_eq!(Some(ChangelogBehaviour{
412                update_changelog: false,
413                explanation: "Assuming changelog does not need to be updated, since there is a [dch] section in gbp.conf.".to_string(),
414        }),
415            guess_update_changelog(&tree, Path::new("debian"), None)
416        );
417    }
418
419    #[test]
420    fn test_changelog_sha_prefixed() {
421        let td = tempfile::tempdir().unwrap();
422        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
423        std::fs::create_dir(td.path().join("debian")).unwrap();
424        std::fs::write(
425            td.path().join("debian/changelog"),
426            r#"blah (0.20.1) unstable; urgency=medium
427
428  [ Somebody ]
429  * [ebb7c31] do a thing
430  * [629746a] do another thing that actually requires us to wrap lines
431    and then
432
433  [ Somebody Else ]
434  * [b02b435] do another thing
435
436 -- Joe User <joe@example.com>  Tue, 19 Nov 2019 15:29:47 +0100
437"#,
438        )
439        .unwrap();
440        tree.add(&[Path::new("debian"), Path::new("debian/changelog")])
441            .unwrap();
442        assert_eq!(
443            Some(ChangelogBehaviour{
444                update_changelog: false,
445                explanation: "Assuming changelog does not need to be updated, since all entries in last changelog entry are prefixed by git shas.".to_string(),
446            }),
447            guess_update_changelog(&tree, Path::new("debian"), None)
448        );
449    }
450
451    #[test]
452    fn test_empty() {
453        let td = tempfile::tempdir().unwrap();
454        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
455        assert_eq!(
456            Some(ChangelogBehaviour{
457                update_changelog: true,
458                explanation: "Assuming changelog needs to be updated, since it is always changed together with other files in the tree.".to_string(),
459            }),
460            guess_update_changelog(&tree, Path::new("debian"), None)
461        );
462    }
463
464    #[test]
465    fn test_update_with_change() {
466        let td = tempfile::tempdir().unwrap();
467        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
468        std::fs::write(td.path().join("upstream"), b"upstream").unwrap();
469        std::fs::create_dir(td.path().join("debian")).unwrap();
470        std::fs::write(
471            td.path().join("debian/changelog"),
472            make_changelog(vec!["initial release".to_string()]),
473        )
474        .unwrap();
475        std::fs::write(td.path().join("debian/control"), b"initial").unwrap();
476        tree.add(&[
477            Path::new("upstream"),
478            Path::new("debian"),
479            Path::new("debian/changelog"),
480            Path::new("debian/control"),
481        ])
482        .unwrap();
483        tree.build_commit()
484            .message("initial release")
485            .committer(COMMITTER)
486            .commit()
487            .unwrap();
488        let mut changelog_entries = vec!["initial release".to_string()];
489        for i in 0..20 {
490            std::fs::write(td.path().join("upstream"), format!("upstream {}", i)).unwrap();
491            changelog_entries.push(format!("next entry {}", i));
492            std::fs::write(
493                td.path().join("debian/changelog"),
494                make_changelog(changelog_entries.clone()),
495            )
496            .unwrap();
497            std::fs::write(td.path().join("debian/control"), format!("next {}", i)).unwrap();
498            tree.build_commit()
499                .committer(COMMITTER)
500                .message("Next")
501                .commit()
502                .unwrap();
503        }
504        assert_eq!(Some(ChangelogBehaviour {
505            update_changelog: true,
506            explanation: "Assuming changelog needs to be updated, since it is always changed together with other files in the tree.".to_string(),
507        }), guess_update_changelog(&tree, Path::new("debian"), None));
508    }
509
510    #[test]
511    fn test_changelog_updated_separately() {
512        let td = tempfile::tempdir().unwrap();
513        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
514        std::fs::create_dir(td.path().join("debian")).unwrap();
515        std::fs::write(
516            td.path().join("debian/changelog"),
517            make_changelog(vec!["initial release".to_string()]),
518        )
519        .unwrap();
520        std::fs::write(td.path().join("debian/control"), b"initial").unwrap();
521        tree.add(&[
522            Path::new("debian"),
523            Path::new("debian/changelog"),
524            Path::new("debian/control"),
525        ])
526        .unwrap();
527        tree.build_commit()
528            .message("initial release")
529            .committer(COMMITTER)
530            .commit()
531            .unwrap();
532        let mut changelog_entries = vec!["initial release".to_string()];
533        for i in 0..20 {
534            changelog_entries.push(format!("next entry {}", i));
535            std::fs::write(
536                td.path().join("debian/control"),
537                format!("next {}", i).as_bytes(),
538            )
539            .unwrap();
540            tree.build_commit()
541                .committer(COMMITTER)
542                .message("Next")
543                .commit()
544                .unwrap();
545        }
546        std::fs::write(
547            td.path().join("debian/changelog"),
548            make_changelog(changelog_entries.clone()),
549        )
550        .unwrap();
551        tree.build_commit()
552            .committer(COMMITTER)
553            .message("Next")
554            .commit()
555            .unwrap();
556        changelog_entries.push("final entry".to_string());
557        std::fs::write(td.path().join("debian/control"), b"more").unwrap();
558        tree.build_commit()
559            .committer(COMMITTER)
560            .message("Next")
561            .commit()
562            .unwrap();
563        std::fs::write(
564            td.path().join("debian/changelog"),
565            make_changelog(changelog_entries),
566        )
567        .unwrap();
568        tree.build_commit()
569            .committer(COMMITTER)
570            .message("Next")
571            .commit()
572            .unwrap();
573        assert_eq!(Some(ChangelogBehaviour{
574            update_changelog: false,
575            explanation: "Assuming changelog does not need to be updated, since changelog entries are usually updated in separate commits.".to_string(),
576        }), guess_update_changelog(&tree, Path::new("debian"), None));
577    }
578
579    #[test]
580    fn test_has_dch_in_messages() {
581        let td = tempfile::tempdir().unwrap();
582        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
583        tree.build_commit()
584            .message("Git-Dch: ignore\n")
585            .allow_pointless(true)
586            .committer(COMMITTER)
587            .commit()
588            .unwrap();
589
590        assert_eq!(Some(ChangelogBehaviour{
591            update_changelog: false,
592            explanation: "Assuming changelog does not need to be updated, since there are Gbp-Dch stanzas in commit messages".to_string(),
593        }), guess_update_changelog(&tree, Path::new("debian"), None));
594    }
595
596    #[test]
597    fn test_inaugural_unreleased() {
598        let td = tempfile::tempdir().unwrap();
599        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
600        std::fs::create_dir(td.path().join("debian")).unwrap();
601        std::fs::write(
602            td.path().join("debian/changelog"),
603            r#"blah (0.20.1) UNRELEASED; urgency=medium
604
605  * Initial release. Closes: #123123
606
607 -- Joe User <joe@example.com>  Tue, 19 Nov 2019 15:29:47 +0100
608"#,
609        )
610        .unwrap();
611        tree.add(&[Path::new("debian"), Path::new("debian/changelog")])
612            .unwrap();
613        assert_eq!(Some(ChangelogBehaviour{
614            update_changelog: false,
615            explanation: "assuming changelog does not need to be updated since it is the inaugural unreleased entry".to_string(),
616        }), guess_update_changelog(&tree, Path::new("debian"), None));
617    }
618
619    #[test]
620    fn test_last_entry_warns_generated() {
621        let td = tempfile::tempdir().unwrap();
622        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
623        std::fs::create_dir(td.path().join("debian")).unwrap();
624        std::fs::write(
625            td.path().join("debian/changelog"),
626            r#"blah (0.20.1) UNRELEASED; urgency=medium
627
628  * WIP (generated at release time: please do not add entries below).
629
630 -- Joe User <joe@example.com>  Tue, 19 Nov 2019 15:29:47 +0100
631
632blah (0.20.1) unstable; urgency=medium
633
634  * Initial release. Closes: #123123
635
636 -- Joe User <joe@example.com>  Tue, 19 Nov 2019 15:29:47 +0100
637"#,
638        )
639        .unwrap();
640        tree.add(&[Path::new("debian"), Path::new("debian/changelog")])
641            .unwrap();
642        assert_eq!(
643            Some(ChangelogBehaviour {
644                update_changelog: false,
645                explanation: "last changelog entry warns changelog is generated at release time"
646                    .to_string()
647            }),
648            guess_update_changelog(&tree, Path::new("debian"), None)
649        );
650    }
651
652    #[test]
653    fn test_never_unreleased() {
654        let td = tempfile::tempdir().unwrap();
655        let tree = create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
656        std::fs::create_dir(td.path().join("debian")).unwrap();
657        std::fs::write(td.path().join("debian/control"), b"foo").unwrap();
658        std::fs::write(
659            td.path().join("debian/changelog"),
660            r#"blah (0.20.1) unstable; urgency=medium
661
662  * Initial release. Closes: #123123
663
664 -- Joe User <joe@example.com>  Tue, 19 Nov 2019 15:29:47 +0100
665"#,
666        )
667        .unwrap();
668
669        tree.add(&[
670            (Path::new("debian")),
671            (Path::new("debian/control")),
672            (Path::new("debian/changelog")),
673        ])
674        .unwrap();
675        tree.build_commit()
676            .committer(COMMITTER)
677            .message("rev1")
678            .commit()
679            .unwrap();
680        std::fs::write(td.path().join("debian/control"), b"bar").unwrap();
681        tree.build_commit()
682            .committer(COMMITTER)
683            .message("rev2")
684            .commit()
685            .unwrap();
686        std::fs::write(td.path().join("debian/control"), b"bla").unwrap();
687        tree.build_commit()
688            .committer(COMMITTER)
689            .message("rev2")
690            .commit()
691            .unwrap();
692        std::fs::write(
693            td.path().join("debian/changelog"),
694            r#"blah (0.21.1) unstable; urgency=medium
695
696  * Next release.
697
698 -- Joe User <joe@example.com>  Tue, 19 Nov 2019 15:29:47 +0100
699
700blah (0.20.1) unstable; urgency=medium
701
702  * Initial release. Closes: #123123
703
704 -- Joe User <joe@example.com>  Tue, 19 Nov 2019 15:29:47 +0100
705"#,
706        )
707        .unwrap();
708        tree.build_commit()
709            .committer(COMMITTER)
710            .message("rev2")
711            .commit()
712            .unwrap();
713        assert_eq!(Some(ChangelogBehaviour{
714            update_changelog: false,
715            explanation: "Assuming changelog does not need to be updated, since it never uses UNRELEASED entries".to_string()
716        }), guess_update_changelog(&tree, Path::new("debian"), None));
717    }
718}