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