Skip to main content

debian_analyzer/
changelog.rs

1//! Functions for working with debian/changelog files.
2use crate::release_info;
3use breezyshim::error::Error;
4use breezyshim::prelude::*;
5use breezyshim::tree::TreeChange;
6use debian_changelog::ChangeLog;
7
8/// Check whether the only change in a tree is to the last changelog entry.
9///
10/// # Arguments
11/// * `tree`: Tree to analyze
12/// * `changelog_path`: Path to the changelog file
13/// * `changes`: Changes in the tree
14pub fn only_changes_last_changelog_block<'a>(
15    tree: &dyn WorkingTree,
16    basis_tree: &dyn Tree,
17    changelog_path: &std::path::Path,
18    changes: impl Iterator<Item = &'a TreeChange>,
19) -> Result<bool, debian_changelog::Error> {
20    let read_lock = tree.lock_read();
21    let basis_lock = basis_tree.lock_read();
22    let mut changes_seen = false;
23    for change in changes {
24        if let Some(path) = change.path.1.as_ref() {
25            if path == std::path::Path::new("") {
26                continue;
27            }
28            if path == changelog_path {
29                changes_seen = true;
30                continue;
31            }
32            if !tree.has_versioned_directories() && changelog_path.starts_with(path) {
33                // Directory leading up to changelog
34                continue;
35            }
36        }
37        // If the change is not in the changelog, it's not just a changelog change
38        return Ok(false);
39    }
40
41    if !changes_seen {
42        // Doesn't change the changelog at all
43        return Ok(false);
44    }
45    // Parse relaxed: this is a structural comparison of the last entry, so a
46    // parse error in old historical entries further down the file must not
47    // make the comparison fail.
48    let mut new_cl = match tree.get_file(changelog_path) {
49        Ok(f) => ChangeLog::read_relaxed(f)?,
50        Err(Error::NoSuchFile(_)) => {
51            return Ok(false);
52        }
53        Err(e) => {
54            panic!("Error reading changelog: {}", e);
55        }
56    };
57    let mut old_cl = match basis_tree.get_file(changelog_path) {
58        Ok(f) => ChangeLog::read_relaxed(f)?,
59        Err(Error::NoSuchFile(_)) => {
60            return Ok(true);
61        }
62        Err(e) => {
63            panic!("Error reading changelog: {}", e);
64        }
65    };
66    let first_entry = if let Some(e) = new_cl.pop_first() {
67        e
68    } else {
69        // No entries
70        return Ok(false);
71    };
72    if first_entry.distributions().as_deref() != Some(&["UNRELEASED".into()]) {
73        // Not unreleased
74        return Ok(false);
75    }
76    old_cl.pop_first();
77    std::mem::drop(read_lock);
78    std::mem::drop(basis_lock);
79    Ok(new_cl.to_string() == old_cl.to_string())
80}
81
82/// Find the last distribution the package was uploaded to.
83pub fn find_last_distribution(cl: &ChangeLog) -> Option<String> {
84    for block in cl.iter() {
85        if block.is_unreleased() != Some(true) {
86            if let Some(distributions) = block.distributions() {
87                if distributions.len() == 1 {
88                    return Some(distributions[0].to_string());
89                }
90            }
91        }
92    }
93    None
94}
95
96/// Given a tree, find the previous upload to the distribution.
97///
98/// When e.g. Ubuntu merges from Debian they want to build with
99/// -vPREV_VERSION. Here's where we find that previous version.
100///
101/// We look at the last changelog entry and find the upload target.
102/// We then search backwards until we find the same target. That's
103/// the previous version that we return.
104///
105/// We require there to be a previous version, otherwise we throw
106/// an error.
107///
108/// It's not a simple string comparison to find the same target in
109/// a previous version, as we should consider old series in e.g.
110/// Ubuntu.
111pub fn find_previous_upload(changelog: &ChangeLog) -> Option<debversion::Version> {
112    let current_target = find_last_distribution(changelog)?;
113    // multiple debian pockets with all debian releases
114    let all_debian = crate::release_info::debian_releases()
115        .iter()
116        .flat_map(|r| {
117            release_info::DEBIAN_POCKETS
118                .iter()
119                .map(move |t| format!("{}{}", r, t))
120        })
121        .collect::<Vec<_>>();
122    let all_ubuntu = crate::release_info::ubuntu_releases()
123        .iter()
124        .flat_map(|r| {
125            release_info::UBUNTU_POCKETS
126                .iter()
127                .map(move |t| format!("{}{}", r, t))
128        })
129        .collect::<Vec<_>>();
130    let match_targets = if all_debian.contains(&current_target) {
131        vec![current_target]
132    } else if all_ubuntu.contains(&current_target) {
133        let mut match_targets = crate::release_info::ubuntu_releases();
134        if current_target.contains('-') {
135            let distro = current_target.split('-').next().unwrap();
136            match_targets.extend(
137                release_info::DEBIAN_POCKETS
138                    .iter()
139                    .map(|r| format!("{}{}", r, distro)),
140            );
141        }
142        match_targets
143    } else {
144        // If we do not recognize the current target in order to apply special
145        // rules to it, then just assume that only previous uploads to exactly
146        // the same target count.
147        vec![current_target]
148    };
149    for block in changelog.iter().skip(1) {
150        if match_targets.contains(&block.distributions().unwrap()[0]) {
151            return block.version().clone();
152        }
153    }
154
155    None
156}
157
158#[derive(Debug)]
159/// Error type for find_changelog
160pub enum FindChangelogError {
161    /// No changelog found in the given files
162    MissingChangelog(Vec<std::path::PathBuf>),
163
164    /// Add a changelog at the given file
165    AddChangelog(std::path::PathBuf),
166
167    /// Error parsing the changelog
168    ChangelogParseError(String),
169
170    /// Error from breezyshim
171    BrzError(breezyshim::error::Error),
172}
173
174impl std::fmt::Display for FindChangelogError {
175    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
176        match self {
177            FindChangelogError::MissingChangelog(files) => {
178                write!(f, "No changelog found in {:?}", files)
179            }
180            FindChangelogError::AddChangelog(file) => {
181                write!(f, "Add a changelog at {:?}", file)
182            }
183            FindChangelogError::ChangelogParseError(e) => write!(f, "{}", e),
184            FindChangelogError::BrzError(e) => write!(f, "{}", e),
185        }
186    }
187}
188
189impl std::error::Error for FindChangelogError {}
190
191impl From<breezyshim::error::Error> for FindChangelogError {
192    fn from(e: breezyshim::error::Error) -> Self {
193        FindChangelogError::BrzError(e)
194    }
195}
196
197/// Find the changelog in the given tree.
198///
199/// First looks for 'debian/changelog'. If "merge" is true will also
200/// look for 'changelog'.
201///
202/// The returned changelog is created with 'allow_empty_author=True'
203/// as some people do this but still want to build.
204/// 'max_blocks' defaults to 1 to try and prevent old broken
205/// changelog entries from causing the command to fail.
206///
207/// "top_level" is a subset of "merge" mode. It indicates that the
208/// '.bzr' dir is at the same level as 'changelog' etc., rather
209/// than being at the same level as 'debian/'.
210///
211/// # Arguments
212/// * `tree`: Tree to look in
213/// * `subpath`: Path to the changelog file
214/// * `merge`: Whether this is a "merge" package
215///
216/// # Returns
217/// * (changelog, top_level) where changelog is the Changelog,
218///   and top_level is a boolean indicating whether the file is
219///   located at 'changelog' (rather than 'debian/changelog') if
220///   merge was given, False otherwise.
221pub fn find_changelog(
222    tree: &dyn Tree,
223    subpath: &std::path::Path,
224    merge: Option<bool>,
225) -> Result<(ChangeLog, bool), FindChangelogError> {
226    let mut top_level = false;
227    let lock = tree.lock_read();
228
229    let mut changelog_file = subpath.join("debian/changelog");
230    if !tree.has_filename(&changelog_file) {
231        let mut checked_files = vec![changelog_file.to_path_buf()];
232        let changelog_file = if merge.unwrap_or(false) {
233            // Assume LarstiQ's layout (.bzr in debian/)
234            let changelog_file = subpath.join("changelog");
235            top_level = true;
236            if !tree.has_filename(&changelog_file) {
237                checked_files.push(changelog_file);
238                None
239            } else {
240                Some(changelog_file)
241            }
242        } else {
243            None
244        };
245        if changelog_file.is_none() {
246            return Err(FindChangelogError::MissingChangelog(checked_files));
247        }
248    } else if merge.unwrap_or(true) && tree.has_filename(&subpath.join("changelog")) {
249        // If it is a "top_level" package and debian is a symlink to
250        // "." then it will have found debian/changelog. Try and detect
251        // this.
252        let debian_file = subpath.join("debian");
253        if tree.is_versioned(&debian_file)
254            && tree.kind(&debian_file)? == breezyshim::tree::Kind::Symlink
255            && tree.get_symlink_target(&debian_file)?.as_path() == std::path::Path::new(".")
256        {
257            changelog_file = "changelog".into();
258            top_level = true;
259        }
260    }
261    log::debug!(
262        "Using '{}' to get package information",
263        changelog_file.display()
264    );
265    if !tree.is_versioned(&changelog_file) {
266        return Err(FindChangelogError::AddChangelog(changelog_file));
267    }
268    let contents = tree.get_file_text(&changelog_file)?;
269    std::mem::drop(lock);
270    let changelog = ChangeLog::read_relaxed(contents.as_slice()).unwrap();
271    Ok((changelog, top_level))
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277    use breezyshim::workingtree::GenericWorkingTree;
278    pub const COMMITTER: &str = "Test User <example@example.com>";
279    #[test]
280    fn test_find_previous_upload() {
281        let cl = r#"test (1.0-1) unstable; urgency=medium
282
283  * Initial release.
284
285 -- Test User <test@user.example.com>  Fri, 01 Jan 2021 00:00:00 +0000
286"#
287        .parse()
288        .unwrap();
289        assert_eq!(super::find_previous_upload(&cl), None);
290
291        let cl = r#"test (1.0-1) unstable; urgency=medium
292
293  * More change.
294
295 -- Test User <test@user.example.com>  Fri, 01 Jan 2021 00:00:00 +0000
296
297test (1.0-0) unstable; urgency=medium
298
299  * Initial release.
300
301 -- Test User <test@example.com>  Fri, 01 Jan 2021 00:00:00 +0000
302"#
303        .parse()
304        .unwrap();
305        assert_eq!(
306            super::find_previous_upload(&cl),
307            Some("1.0-0".parse().unwrap())
308        );
309    }
310
311    mod test_only_changes_last_changelog_block {
312        use super::*;
313        use breezyshim::controldir::{create_standalone_workingtree, ControlDirFormat};
314        use breezyshim::tree::Path;
315        fn make_package_tree(p: &std::path::Path) -> GenericWorkingTree {
316            let tree = create_standalone_workingtree(p, &ControlDirFormat::default()).unwrap();
317            std::fs::create_dir_all(p.join("debian")).unwrap();
318
319            std::fs::write(
320                p.join("debian/control"),
321                r###"Source: blah
322Vcs-Git: https://example.com/blah
323Testsuite: autopkgtest
324
325Binary: blah
326Arch: all
327
328"###,
329            )
330            .unwrap();
331            std::fs::write(
332                p.join("debian/changelog"),
333                r###"blah (0.2) UNRELEASED; urgency=medium
334
335  * And a change.
336
337 -- Blah <example@debian.org>  Sat, 13 Oct 2018 11:21:39 +0100
338
339blah (0.1) unstable; urgency=medium
340
341  * Initial release. (Closes: #911016)
342
343 -- Blah <example@debian.org>  Sat, 13 Oct 2018 11:21:39 +0100
344"###,
345            )
346            .unwrap();
347            tree.add(&[
348                Path::new("debian"),
349                Path::new("debian/changelog"),
350                Path::new("debian/control"),
351            ])
352            .unwrap();
353            tree.build_commit()
354                .message("Initial thingy.")
355                .committer(COMMITTER)
356                .commit()
357                .unwrap();
358            tree
359        }
360
361        #[test]
362        fn test_no_changes() {
363            let td = tempfile::tempdir().unwrap();
364            let tree = make_package_tree(td.path());
365            let basis_tree = tree.basis_tree().unwrap();
366            let lock_read = tree.lock_read();
367            let basis_lock_read = basis_tree.lock_read();
368            let changes = tree
369                .iter_changes(&basis_tree, None, None, None)
370                .unwrap()
371                .collect::<Result<Vec<_>, _>>()
372                .unwrap();
373            assert!(!only_changes_last_changelog_block(
374                &tree,
375                &tree.basis_tree().unwrap(),
376                Path::new("debian/changelog"),
377                changes.iter()
378            )
379            .unwrap());
380            std::mem::drop(basis_lock_read);
381            std::mem::drop(lock_read);
382        }
383
384        #[test]
385        fn test_other_change() {
386            let td = tempfile::tempdir().unwrap();
387            let tree = make_package_tree(td.path());
388            std::fs::write(
389                td.path().join("debian/control"),
390                r###"Source: blah
391Vcs-Bzr: https://example.com/blah
392Testsuite: autopkgtest
393
394Binary: blah
395Arch: all
396"###,
397            )
398            .unwrap();
399            let basis_tree = tree.basis_tree().unwrap();
400            let lock_read = tree.lock_read();
401            let basis_lock_read = basis_tree.lock_read();
402            let changes = tree
403                .iter_changes(&basis_tree, None, None, None)
404                .unwrap()
405                .collect::<Result<Vec<_>, _>>()
406                .unwrap();
407            assert!(!only_changes_last_changelog_block(
408                &tree,
409                &tree.basis_tree().unwrap(),
410                Path::new("debian/changelog"),
411                changes.iter()
412            )
413            .unwrap());
414            std::mem::drop(basis_lock_read);
415            std::mem::drop(lock_read);
416        }
417
418        #[test]
419        fn test_other_changes() {
420            let td = tempfile::tempdir().unwrap();
421            let tree = make_package_tree(td.path());
422            std::fs::write(
423                td.path().join("debian/control"),
424                r###"Source: blah
425Vcs-Bzr: https://example.com/blah
426Testsuite: autopkgtest
427
428Binary: blah
429Arch: all
430
431"###,
432            )
433            .unwrap();
434            std::fs::write(
435                td.path().join("debian/changelog"),
436                r###"blah (0.1) UNRELEASED; urgency=medium
437
438  * Initial release. (Closes: #911016)
439  * Some other change.
440
441 -- Blah <example@debian.org>  Sat, 13 Oct 2018 11:21:39 +0100
442"###,
443            )
444            .unwrap();
445            let basis_tree = tree.basis_tree().unwrap();
446            let lock_read = tree.lock_read();
447            let basis_lock_read = basis_tree.lock_read();
448            let changes = tree
449                .iter_changes(&basis_tree, None, None, None)
450                .unwrap()
451                .collect::<Result<Vec<_>, _>>()
452                .unwrap();
453            assert!(!only_changes_last_changelog_block(
454                &tree,
455                &tree.basis_tree().unwrap(),
456                Path::new("debian/changelog"),
457                changes.iter()
458            )
459            .unwrap());
460            std::mem::drop(basis_lock_read);
461            std::mem::drop(lock_read);
462        }
463
464        #[test]
465        fn test_changes_to_other_changelog_entries() {
466            let td = tempfile::tempdir().unwrap();
467            let tree = make_package_tree(td.path());
468            std::fs::write(
469                td.path().join("debian/changelog"),
470                r###"blah (0.2) UNRELEASED; urgency=medium
471
472  * debian/changelog: And a change.
473
474 -- Blah <example@debian.org>  Sat, 13 Oct 2018 11:21:39 +0100
475
476blah (0.1) unstable; urgency=medium
477
478  * debian/changelog: Initial release. (Closes: #911016)
479
480 -- Blah <example@debian.org>  Sat, 13 Oct 2018 11:21:39 +0100
481"###,
482            )
483            .unwrap();
484            let basis_tree = tree.basis_tree().unwrap();
485            let lock_read = tree.lock_read();
486            let basis_lock_read = basis_tree.lock_read();
487            let changes = tree
488                .iter_changes(&basis_tree, None, None, None)
489                .unwrap()
490                .collect::<Result<Vec<_>, _>>()
491                .unwrap();
492            assert!(!only_changes_last_changelog_block(
493                &tree,
494                &tree.basis_tree().unwrap(),
495                Path::new("debian/changelog"),
496                changes.iter()
497            )
498            .unwrap());
499            std::mem::drop(basis_lock_read);
500            std::mem::drop(lock_read);
501        }
502
503        #[test]
504        fn test_changes_to_last_only() {
505            let td = tempfile::tempdir().unwrap();
506            let tree = make_package_tree(td.path());
507            std::fs::write(
508                td.path().join("debian/changelog"),
509                r###"blah (0.2) UNRELEASED; urgency=medium
510
511  * And a change.
512  * Not a team upload.
513
514 -- Blah <example@debian.org>  Sat, 13 Oct 2018 11:21:39 +0100
515
516blah (0.1) unstable; urgency=medium
517
518  * Initial release. (Closes: #911016)
519
520 -- Blah <example@debian.org>  Sat, 13 Oct 2018 11:21:39 +0100
521"###,
522            )
523            .unwrap();
524            let basis_tree = tree.basis_tree().unwrap();
525            let lock_read = tree.lock_read();
526            let basis_lock_read = basis_tree.lock_read();
527            let changes = tree
528                .iter_changes(&basis_tree, None, None, None)
529                .unwrap()
530                .collect::<Result<Vec<_>, _>>()
531                .unwrap();
532            assert!(only_changes_last_changelog_block(
533                &tree,
534                &tree.basis_tree().unwrap(),
535                Path::new("debian/changelog"),
536                changes.iter()
537            )
538            .unwrap());
539            std::mem::drop(basis_lock_read);
540            std::mem::drop(lock_read);
541        }
542
543        #[test]
544        fn test_only_new_changelog() {
545            use breezyshim::tree::MutableTree;
546            let td = tempfile::tempdir().unwrap();
547            let tree = create_standalone_workingtree(td.path(), "git").unwrap();
548            let lock_write = tree.lock_write();
549            std::fs::create_dir_all(td.path().join("debian")).unwrap();
550            std::fs::write(
551                td.path().join("debian/changelog"),
552                r###"blah (0.1) unstable; urgency=medium
553
554  * Initial release. (Closes: #911016)
555
556 -- Blah <example@debian.org>  Sat, 13 Oct 2018 11:21:39 +0100
557"###,
558            )
559            .unwrap();
560            let basis_tree = tree.basis_tree().unwrap();
561            let lock_read = tree.lock_read();
562            let basis_lock_read = basis_tree.lock_read();
563            tree.add(&[Path::new("debian"), Path::new("debian/changelog")])
564                .unwrap();
565            let changes = tree
566                .iter_changes(&basis_tree, None, None, None)
567                .unwrap()
568                .collect::<Result<Vec<_>, _>>()
569                .unwrap();
570            assert!(only_changes_last_changelog_block(
571                &tree,
572                &tree.basis_tree().unwrap(),
573                Path::new("debian/changelog"),
574                changes.iter()
575            )
576            .unwrap());
577            std::mem::drop(basis_lock_read);
578            std::mem::drop(lock_read);
579            std::mem::drop(lock_write);
580        }
581
582        #[test]
583        fn test_changes_to_last_only_but_released() {
584            let td = tempfile::tempdir().unwrap();
585            let tree = make_package_tree(td.path());
586            std::fs::write(
587                td.path().join("debian/changelog"),
588                r###"blah (0.2) unstable; urgency=medium
589
590  * And a change.
591
592 -- Blah <example@debian.org>  Sat, 13 Oct 2018 11:21:39 +0100
593
594blah (0.1) unstable; urgency=medium
595
596  * Initial release. (Closes: #911016)
597
598 -- Blah <example@debian.org>  Sat, 13 Oct 2018 11:21:39 +0100
599"###,
600            )
601            .unwrap();
602            tree.build_commit()
603                .message("release")
604                .committer(COMMITTER)
605                .commit()
606                .unwrap();
607            std::fs::write(
608                td.path().join("debian/changelog"),
609                r###"blah (0.2) unstable; urgency=medium
610
611  * And a change.
612  * Team Upload.
613
614 -- Blah <example@debian.org>  Sat, 13 Oct 2018 11:21:39 +0100
615
616blah (0.1) unstable; urgency=medium
617
618  * Initial release. (Closes: #911016)
619
620 -- Blah <example@debian.org>  Sat, 13 Oct 2018 11:21:39 +0100
621"###,
622            )
623            .unwrap();
624            let basis_tree = tree.basis_tree().unwrap();
625            let lock_read = tree.lock_read();
626            let basis_lock_read = basis_tree.lock_read();
627            let changes = tree
628                .iter_changes(&basis_tree, None, None, None)
629                .unwrap()
630                .collect::<Result<Vec<_>, _>>()
631                .unwrap();
632
633            assert!(!only_changes_last_changelog_block(
634                &tree,
635                &tree.basis_tree().unwrap(),
636                Path::new("debian/changelog"),
637                changes.iter()
638            )
639            .unwrap());
640            std::mem::drop(basis_lock_read);
641            std::mem::drop(lock_read);
642        }
643
644        #[test]
645        fn test_changes_to_last_only_with_old_style_trailing_entries() {
646            // A changelog that ends with pre-1.0 old-style entries does not
647            // parse cleanly under the strict reader. The comparison only cares
648            // about the last block, so such trailing content must not make it
649            // fail.
650            let td = tempfile::tempdir().unwrap();
651            let tree =
652                create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
653            std::fs::create_dir_all(td.path().join("debian")).unwrap();
654            let changelog = r###"blah (0.2) UNRELEASED; urgency=medium
655
656  * And a change.
657
658 -- Blah <example@debian.org>  Sat, 13 Oct 2018 11:21:39 +0100
659
660blah (0.1) unstable; urgency=medium
661
662  * Initial release. (Closes: #911016)
663
664 -- Blah <example@debian.org>  Sat, 13 Oct 2018 11:21:39 +0100
665
6660.0.1-1:
667 19940101
668 * Old-style changelog entry.
669"###;
670            std::fs::write(td.path().join("debian/changelog"), changelog).unwrap();
671            tree.add(&[Path::new("debian"), Path::new("debian/changelog")])
672                .unwrap();
673            tree.build_commit()
674                .message("Initial thingy.")
675                .committer(COMMITTER)
676                .commit()
677                .unwrap();
678            std::fs::write(
679                td.path().join("debian/changelog"),
680                changelog.replace(
681                    "  * And a change.\n",
682                    "  * And a change.\n  * Another change.\n",
683                ),
684            )
685            .unwrap();
686            let basis_tree = tree.basis_tree().unwrap();
687            let lock_read = tree.lock_read();
688            let basis_lock_read = basis_tree.lock_read();
689            let changes = tree
690                .iter_changes(&basis_tree, None, None, None)
691                .unwrap()
692                .collect::<Result<Vec<_>, _>>()
693                .unwrap();
694            assert!(only_changes_last_changelog_block(
695                &tree,
696                &tree.basis_tree().unwrap(),
697                Path::new("debian/changelog"),
698                changes.iter()
699            )
700            .unwrap());
701            std::mem::drop(basis_lock_read);
702            std::mem::drop(lock_read);
703        }
704    }
705}