debian_analyzer/
patches.rs

1//! Functions for working with patches in a Debian package.
2use breezyshim::delta::filter_excluded;
3use breezyshim::error::Error as BrzError;
4use breezyshim::patches::AppliedPatches;
5use breezyshim::prelude::*;
6use breezyshim::tree::{PyTree, Tree};
7use breezyshim::workingtree::{PyWorkingTree, WorkingTree};
8use breezyshim::workspace::reset_tree_with_dirty_tracker;
9use breezyshim::RevisionId;
10use debian_changelog::ChangeLog;
11use patchkit::quilt::QuiltPatch;
12use patchkit::unified::UnifiedPatch;
13use std::io::Write;
14use std::path::{Path, PathBuf};
15
16// TODO(jelmer): Use debmutate version
17/// Default name of the patches directory.
18pub const DEFAULT_DEBIAN_PATCHES_DIR: &str = "debian/patches";
19
20/// Find the name of the patches directory.
21///
22/// This will always return a path, even if the patches directory does not yet exist.
23///
24/// # Arguments
25///
26/// * `tree` - Tree to check
27/// * `subpath` - Subpath to check
28///
29/// # Returns
30///
31/// Path to patches directory, or what it should be
32pub fn tree_patches_directory(tree: &dyn PyTree, subpath: &Path) -> PathBuf {
33    find_patches_directory(tree, subpath).unwrap_or(DEFAULT_DEBIAN_PATCHES_DIR.into())
34}
35
36#[cfg(test)]
37mod tree_patches_directory_tests {
38    use super::*;
39
40    #[test]
41    fn test_simple() {
42        let td = tempfile::tempdir().unwrap();
43        let local_tree = breezyshim::controldir::create_standalone_workingtree(
44            td.path(),
45            &breezyshim::controldir::ControlDirFormat::default(),
46        )
47        .unwrap();
48        assert_eq!(
49            super::tree_patches_directory(&local_tree, std::path::Path::new("")),
50            std::path::Path::new("debian/patches")
51        );
52    }
53
54    #[test]
55    fn test_default() {
56        let td = tempfile::tempdir().unwrap();
57        let local_tree = breezyshim::controldir::create_standalone_workingtree(
58            td.path(),
59            &breezyshim::controldir::ControlDirFormat::default(),
60        )
61        .unwrap();
62        local_tree.mkdir(std::path::Path::new("debian")).unwrap();
63        local_tree
64            .mkdir(std::path::Path::new("debian/patches"))
65            .unwrap();
66        assert_eq!(
67            super::tree_patches_directory(&local_tree, std::path::Path::new("")),
68            std::path::Path::new("debian/patches")
69        );
70    }
71
72    #[test]
73    fn test_custom() {
74        let td = tempfile::tempdir().unwrap();
75        let local_tree = breezyshim::controldir::create_standalone_workingtree(
76            td.path(),
77            &breezyshim::controldir::ControlDirFormat::default(),
78        )
79        .unwrap();
80        local_tree.mkdir(std::path::Path::new("debian")).unwrap();
81        local_tree
82            .mkdir(std::path::Path::new("debian/patches"))
83            .unwrap();
84        local_tree
85            .put_file_bytes_non_atomic(
86                std::path::Path::new("debian/rules"),
87                br#"
88QUILT_PATCH_DIR := debian/patches-applied
89
90all:
91
92blah: bloe
93	foo
94
95"#,
96            )
97            .unwrap();
98        assert_eq!(
99            super::tree_patches_directory(&local_tree, std::path::Path::new("")),
100            std::path::Path::new("debian/patches-applied")
101        );
102    }
103}
104
105/// Find the name of the patches directory in a debian/rules file
106pub fn rules_find_patches_directory(mf: &makefile_lossless::Makefile) -> Option<PathBuf> {
107    mf.variable_definitions()
108        .find(|v| v.name().as_deref() == Some("QUILT_PATCH_DIR"))?
109        .raw_value()
110        .map(PathBuf::from)
111}
112
113#[test]
114fn test_rules_find_patches_directory() {
115    let mf = makefile_lossless::Makefile::read_relaxed(
116        &br#"QUILT_PATCH_DIR := debian/patches-applied
117"#[..],
118    )
119    .unwrap();
120    assert_eq!(
121        rules_find_patches_directory(&mf),
122        Some(PathBuf::from("debian/patches-applied"))
123    );
124}
125
126/// Find the patches directory for a package
127pub fn find_patches_directory(tree: &dyn PyTree, subpath: &Path) -> Option<PathBuf> {
128    let rules_path = subpath.join("debian/rules");
129
130    let rules_file = match tree.get_file(&rules_path) {
131        Ok(f) => Some(f),
132        Err(BrzError::NoSuchFile(_)) => None,
133        Err(e) => {
134            log::warn!("Failed to read {}: {}", rules_path.display(), e);
135            None
136        }
137    };
138
139    if let Some(rules_file) = rules_file {
140        let mf_patch_dir = match makefile_lossless::Makefile::read_relaxed(rules_file) {
141            Ok(mf) => rules_find_patches_directory(&mf).or_else(|| {
142                log::debug!("No QUILT_PATCH_DIR in {}", rules_path.display());
143                None
144            }),
145            Err(e) => {
146                log::warn!("Failed to parse {}: {}", rules_path.display(), e);
147                None
148            }
149        };
150
151        if let Some(mf_patch_dir) = mf_patch_dir {
152            return Some(mf_patch_dir);
153        }
154    }
155
156    if tree.has_filename(Path::new(DEFAULT_DEBIAN_PATCHES_DIR)) {
157        return Some(DEFAULT_DEBIAN_PATCHES_DIR.into());
158    }
159
160    None
161}
162
163/// Find the base revision to apply patches to.
164///
165/// * `tree` - Tree to find the patch base for
166pub fn find_patch_base(tree: &dyn WorkingTree) -> Option<RevisionId> {
167    let f = match tree.get_file(std::path::Path::new("debian/changelog")) {
168        Ok(f) => f,
169        Err(BrzError::NoSuchFile(_)) => return None,
170        Err(e) => {
171            log::warn!("Failed to read debian/changelog: {}", e);
172            return None;
173        }
174    };
175    let cl = match ChangeLog::read(f) {
176        Ok(cl) => cl,
177        Err(e) => {
178            log::warn!("Failed to parse debian/changelog: {}", e);
179            return None;
180        }
181    };
182    let entry = cl.iter().next()?;
183    let package = entry.package().unwrap();
184    let upstream_version = entry.version().unwrap().upstream_version;
185    let possible_tags = [
186        format!("upstream-{}", upstream_version),
187        format!("upstream/{}", upstream_version),
188        upstream_version.to_string(),
189        format!("v{}", upstream_version),
190        format!("{}-{}", package, upstream_version),
191    ];
192    let tags = tree.branch().tags().unwrap().get_tag_dict().unwrap();
193    possible_tags.iter().find_map(|tag| tags.get(tag).cloned())
194}
195
196#[cfg(test)]
197mod find_patch_base_tests {
198    const COMMITTER: &str = "Test Suite <test@suite.example.com>";
199    use super::*;
200    use breezyshim::tree::{MutableTree, WorkingTree};
201    use breezyshim::workingtree::GenericWorkingTree;
202    use breezyshim::RevisionId;
203
204    fn setup() -> (tempfile::TempDir, GenericWorkingTree, RevisionId) {
205        let td = tempfile::tempdir().unwrap();
206        let tree = breezyshim::controldir::create_standalone_workingtree(
207            td.path(),
208            &breezyshim::controldir::ControlDirFormat::default(),
209        )
210        .unwrap();
211        let upstream_revid = tree
212            .build_commit()
213            .message("upstream")
214            .committer(COMMITTER)
215            .commit()
216            .unwrap();
217        tree.mkdir(std::path::Path::new("debian")).unwrap();
218        std::fs::write(
219            td.path().join("debian/changelog"),
220            r#"blah (0.38) unstable; urgency=medium
221
222  * Fix something
223
224 -- Jelmer Vernooij <jelmer@debian.org>  Sat, 19 Oct 2019 15:21:53 +0000
225"#,
226        )
227        .unwrap();
228        tree.add(&[std::path::Path::new("debian/changelog")])
229            .unwrap();
230        (td, tree, upstream_revid)
231    }
232
233    #[test]
234    fn test_none() {
235        let (td, tree, _upstream_revid) = setup();
236        assert_eq!(None, super::find_patch_base(&tree));
237        std::mem::drop(td);
238    }
239
240    #[test]
241    fn test_upstream_dash() {
242        let (td, tree, upstream_revid) = setup();
243        tree.branch()
244            .tags()
245            .unwrap()
246            .set_tag("upstream-0.38", &upstream_revid)
247            .unwrap();
248        let tags = tree.branch().tags().unwrap().get_tag_dict().unwrap();
249        assert_eq!(Some(&upstream_revid), tags.get("upstream-0.38"));
250        assert_eq!(Some(upstream_revid), super::find_patch_base(&tree));
251        std::mem::drop(td);
252    }
253}
254
255/// Find the branch that is used to track patches.
256///
257/// * `tree` - Tree for which to find patches branch
258///
259/// Returns:
260/// A `Branch` instance
261pub fn find_patches_branch(tree: &dyn WorkingTree) -> Option<Box<dyn Branch>> {
262    let local_branch_name = tree.branch().name()?;
263    let branch_name = format!("patch-queue/{}", local_branch_name);
264    match tree
265        .branch()
266        .controldir()
267        .open_branch(Some(branch_name.as_str()))
268    {
269        Ok(b) => return Some(b),
270        Err(BrzError::NotBranchError(..)) => {}
271        Err(e) => {
272            log::warn!("Failed to open branch {}: {}", branch_name, e);
273        }
274    }
275    let branch_name = if local_branch_name == "master" {
276        "patched".to_string()
277    } else {
278        format!("patched-{}", local_branch_name)
279    };
280    match tree
281        .branch()
282        .controldir()
283        .open_branch(Some(branch_name.as_str()))
284    {
285        Ok(b) => return Some(b),
286        Err(BrzError::NotBranchError(..)) => {}
287        Err(e) => {
288            log::warn!("Failed to open branch {}: {}", branch_name, e);
289        }
290    }
291    None
292}
293
294#[cfg(test)]
295mod find_patches_branch_tests {
296    use super::*;
297    use breezyshim::workingtree::{GenericWorkingTree, WorkingTree};
298
299    fn make_named_branch_and_tree(name: &str) -> (tempfile::TempDir, GenericWorkingTree) {
300        let td = tempfile::tempdir().unwrap();
301        let dir = breezyshim::controldir::create(
302            &url::Url::from_directory_path(td.path()).unwrap(),
303            &breezyshim::controldir::ControlDirFormat::default(),
304            None,
305        )
306        .unwrap();
307        dir.create_repository(None).unwrap();
308        let branch = dir.create_branch(Some(name)).unwrap();
309        dir.set_branch_reference(branch.as_ref(), None).unwrap();
310        let wt = dir.create_workingtree().unwrap();
311        (td, wt)
312    }
313
314    #[test]
315    fn test_none() {
316        let td = tempfile::tempdir().unwrap();
317        let local_tree = breezyshim::controldir::create_standalone_workingtree(
318            td.path(),
319            &breezyshim::controldir::ControlDirFormat::default(),
320        )
321        .unwrap();
322        assert!(super::find_patches_branch(&local_tree).is_none());
323    }
324
325    #[test]
326    fn test_patch_queue() {
327        let (td, master) = make_named_branch_and_tree("master");
328        master
329            .branch()
330            .controldir()
331            .create_branch(Some("patch-queue/master"))
332            .unwrap();
333
334        assert_eq!(
335            "patch-queue/master",
336            super::find_patches_branch(&master)
337                .unwrap()
338                .name()
339                .unwrap()
340                .as_str()
341        );
342
343        std::mem::drop(td);
344    }
345
346    #[test]
347    fn test_patched_master() {
348        let (td, master) = make_named_branch_and_tree("master");
349        master
350            .branch()
351            .controldir()
352            .create_branch(Some("patched"))
353            .unwrap();
354        assert_eq!(
355            "patched",
356            super::find_patches_branch(&master).unwrap().name().unwrap()
357        );
358        std::mem::drop(td);
359    }
360
361    #[test]
362    fn test_patched_other() {
363        let (td, other) = make_named_branch_and_tree("other");
364        other
365            .branch()
366            .controldir()
367            .create_branch(Some("patched-other"))
368            .unwrap();
369        assert_eq!(
370            "patched-other",
371            super::find_patches_branch(&other).unwrap().name().unwrap()
372        );
373        std::mem::drop(td);
374    }
375}
376
377/// Add a new patch.
378///
379/// # Arguments
380/// * `tree` - Tree to edit
381/// * `patches_directory` - Name of patches directory
382/// * `name` - Patch name without suffix
383/// * `contents` - Diff
384/// * `header` - RFC822 to read
385///
386/// Returns:
387/// Name of the patch that was written (including suffix)
388pub fn add_patch(
389    tree: &dyn PyWorkingTree,
390    patches_directory: &Path,
391    name: &str,
392    contents: &[u8],
393    header: Option<dep3::lossless::PatchHeader>,
394) -> Result<(Vec<std::path::PathBuf>, String), String> {
395    if !tree.has_filename(patches_directory) {
396        let parent = patches_directory.parent().unwrap();
397        if !tree.has_filename(parent) {
398            tree.mkdir(parent)
399                .expect("Failed to create parent directory");
400        }
401        tree.mkdir(patches_directory).unwrap();
402    }
403    let series_path = patches_directory.join("series");
404    let mut series = match tree.get_file(&series_path) {
405        Ok(f) => patchkit::quilt::Series::read(f).unwrap(),
406        Err(BrzError::NoSuchFile(_)) => patchkit::quilt::Series::new(),
407        Err(e) => {
408            return Err(format!("Failed to read {}: {}", series_path.display(), e));
409        }
410    };
411
412    let patch_suffix =
413        patchkit::quilt::find_common_patch_suffix(series.patches()).unwrap_or(".patch");
414    let patchname = format!("{}{}", name, patch_suffix);
415    let path = patches_directory.join(patchname.as_str());
416    if tree.has_filename(path.as_path()) {
417        return Err(format!("Patch {} already exists", patchname));
418    }
419
420    let mut patch_contents = Vec::new();
421    if let Some(header) = header {
422        header.write(&mut patch_contents).unwrap();
423    }
424    patch_contents.write_all(b"---\n").unwrap();
425    patch_contents.write_all(contents).unwrap();
426    tree.put_file_bytes_non_atomic(&path, patch_contents.as_slice())
427        .map_err(|e| format!("Failed to write patch: {}", e))?;
428
429    // TODO(jelmer): Write to patches branch if applicable
430
431    series.append(patchname.as_str(), None);
432    let mut series_bytes = Vec::new();
433    series
434        .write(&mut series_bytes)
435        .map_err(|e| format!("Failed to write series: {}", e))?;
436    tree.put_file_bytes_non_atomic(&series_path, series_bytes.as_slice())
437        .map_err(|e| format!("Failed to write series: {}", e))?;
438    tree.add(&[series_path.as_path(), path.as_path()])
439        .map_err(|e| format!("Failed to add patch: {}", e))?;
440
441    let specific_files = vec![series_path, path];
442
443    Ok((specific_files, patchname))
444}
445
446/// Move upstream changes to patch.
447///
448/// # Arguments
449///
450/// * `local_tree` - Local tree
451/// * `basis_tree` - Basis tree
452/// * `subpath` - Subpath
453/// * `patch_name` - Suggested patch name
454/// * `description` - Description
455pub fn move_upstream_changes_to_patch<T, U>(
456    local_tree: &T,
457    basis_tree: &U,
458    subpath: &std::path::Path,
459    patch_name: &str,
460    description: &str,
461    dirty_tracker: Option<&mut breezyshim::dirty_tracker::DirtyTreeTracker>,
462    timestamp: Option<chrono::NaiveDate>,
463) -> Result<(Vec<std::path::PathBuf>, String), String>
464where
465    T: PyWorkingTree,
466    U: PyTree,
467{
468    let timestamp = if let Some(timestamp) = timestamp {
469        timestamp
470    } else {
471        chrono::Utc::now().naive_utc().date()
472    };
473    let mut diff = Vec::new();
474    breezyshim::diff::show_diff_trees(basis_tree, local_tree, &mut diff, None, None)
475        .map_err(|e| format!("Failed to generate diff: {}", e))?;
476    reset_tree_with_dirty_tracker(local_tree, Some(basis_tree), Some(subpath), dirty_tracker)
477        .map_err(|e| format!("Failed to reset tree: {}", e))?;
478    // See https://dep-team.pages.debian.net/deps/dep3/ for fields.
479    let mut dep3_header = dep3::lossless::PatchHeader::new();
480    dep3_header.set_description(description);
481    dep3_header.set_origin(None, dep3::Origin::Other("other".into()));
482    dep3_header.set_last_update(timestamp);
483    let patches_directory = subpath.join(tree_patches_directory(local_tree, subpath));
484    let (specific_files, patchname) = add_patch(
485        local_tree,
486        &patches_directory,
487        patch_name,
488        diff.as_slice(),
489        Some(dep3_header),
490    )?;
491    Ok((specific_files, patchname))
492}
493
494#[cfg(test)]
495mod move_upstream_changes_to_patch_tests {
496    use super::*;
497    use breezyshim::controldir::ControlDirFormat;
498    use breezyshim::tree::MutableTree;
499
500    #[test]
501    fn test_simple() {
502        breezyshim::init();
503        let td = tempfile::tempdir().unwrap();
504        let local_tree = breezyshim::controldir::create_standalone_workingtree(
505            td.path(),
506            &ControlDirFormat::default(),
507        )
508        .unwrap();
509
510        std::fs::write(td.path().join("foo"), b"foo\n").unwrap();
511        local_tree.mkdir(std::path::Path::new("debian")).unwrap();
512        local_tree.add(&[std::path::Path::new("foo")]).unwrap();
513
514        super::move_upstream_changes_to_patch(
515            &local_tree,
516            &local_tree.basis_tree().unwrap(),
517            std::path::Path::new(""),
518            "patch",
519            "This is a description",
520            None,
521            Some(chrono::NaiveDate::from_ymd_opt(2020, 1, 1).unwrap()),
522        )
523        .unwrap();
524
525        let path = td.path();
526
527        assert!(!path.join("foo").exists());
528        assert!(path.join("debian/patches").exists());
529        assert!(path.join("debian/patches/series").exists());
530        assert!(path.join("debian/patches/patch.patch").exists());
531
532        let series = std::fs::read_to_string(path.join("debian/patches/series")).unwrap();
533        assert_eq!(series, "patch.patch\n");
534
535        let patch = std::fs::read_to_string(path.join("debian/patches/patch.patch")).unwrap();
536        assert!(
537            patch.starts_with(
538                r#"Description: This is a description
539Origin: other
540Last-Update: 2020-01-01
541---
542"#
543            ),
544            "{:?}",
545            patch
546        );
547
548        assert!(
549            patch.ends_with(
550                r#"@@ -0,0 +1,1 @@
551+foo
552
553"#
554            ),
555            "{:?}",
556            patch
557        );
558    }
559}
560
561/// Read quilt patches from a directory.
562pub fn read_quilt_patches<'a>(
563    tree: &'a dyn Tree,
564    directory: &'a std::path::Path,
565) -> impl Iterator<Item = UnifiedPatch> + 'a {
566    let series_path = directory.join("series");
567    let series = match tree.get_file(series_path.as_path()) {
568        Ok(series) => patchkit::quilt::Series::read(series).unwrap(),
569        Err(BrzError::NoSuchFile(..)) => patchkit::quilt::Series::new(),
570        Err(e) => panic!("error reading series: {:?}", e),
571    };
572
573    let mut ret = vec![];
574    for entry in series.iter() {
575        let (patch, options) = match entry {
576            patchkit::quilt::SeriesEntry::Patch {
577                name: patch,
578                options,
579            } => (patch, options),
580            patchkit::quilt::SeriesEntry::Comment(_) => continue,
581        };
582        let p = directory.join(patch);
583        let lines = tree.get_file_text(p.as_path()).unwrap();
584        let patch = QuiltPatch {
585            name: patch.to_string(),
586            patch: lines,
587            options: options.to_vec(),
588        };
589        ret.push(patch);
590    }
591    ret.into_iter().flat_map(|p| p.parse().unwrap())
592}
593
594#[cfg(test)]
595mod read_quilt_patches_tests {
596    const COMMITTER: &str = "Test Suite <test@suite.example.com>";
597    use super::*;
598    use breezyshim::controldir::ControlDirFormat;
599    use breezyshim::tree::MutableTree;
600
601    #[test]
602    fn test_read_patches() {
603        let patch = "\
604--- a/a
605+++ b/a
606@@ -1,5 +1,5 @@
607 line 1
608 line 2
609-line 3
610+new line 3
611 line 4
612 line 5
613";
614        breezyshim::init();
615        let td = tempfile::tempdir().unwrap();
616        let tree = breezyshim::controldir::create_standalone_workingtree(
617            td.path(),
618            &ControlDirFormat::default(),
619        )
620        .unwrap();
621        tree.mkdir(std::path::Path::new("debian")).unwrap();
622        tree.mkdir(std::path::Path::new("debian/patches")).unwrap();
623        std::fs::write(td.path().join("debian/patches/series"), "foo\n").unwrap();
624        std::fs::write(td.path().join("debian/patches/foo"), patch).unwrap();
625        tree.add(
626            [
627                "debian",
628                "debian/patches",
629                "debian/patches/series",
630                "debian/patches/foo",
631            ]
632            .into_iter()
633            .map(std::path::Path::new)
634            .collect::<Vec<_>>()
635            .as_slice(),
636        )
637        .unwrap();
638        tree.build_commit()
639            .message("add patch")
640            .committer(COMMITTER)
641            .commit()
642            .unwrap();
643        let patches = super::read_quilt_patches(&tree, std::path::Path::new("debian/patches"))
644            .collect::<Vec<_>>();
645        assert_eq!(1, patches.len());
646        assert_eq!(patch, std::str::from_utf8(&patches[0].as_bytes()).unwrap());
647    }
648
649    #[test]
650    fn test_no_series_file() {
651        breezyshim::init();
652        let td = tempfile::tempdir().unwrap();
653        let tree = breezyshim::controldir::create_standalone_workingtree(
654            td.path(),
655            &ControlDirFormat::default(),
656        )
657        .unwrap();
658        let patches = super::read_quilt_patches(&tree, std::path::Path::new("debian/patches"))
659            .collect::<Vec<_>>();
660        assert_eq!(0, patches.len());
661    }
662
663    #[test]
664    fn test_comments() {
665        let td = tempfile::tempdir().unwrap();
666        let tree = breezyshim::controldir::create_standalone_workingtree(
667            td.path(),
668            &ControlDirFormat::default(),
669        )
670        .unwrap();
671        tree.mkdir(std::path::Path::new("debian")).unwrap();
672        tree.mkdir(std::path::Path::new("debian/patches")).unwrap();
673        tree.put_file_bytes_non_atomic(
674            std::path::Path::new("debian/patches/series"),
675            b"# This file intentionally left blank.\n",
676        )
677        .unwrap();
678        tree.add(&[std::path::Path::new("debian/patches/series")])
679            .unwrap();
680        tree.build_commit()
681            .message("add series")
682            .committer(COMMITTER)
683            .commit()
684            .unwrap();
685        let patches = super::read_quilt_patches(&tree, std::path::Path::new("debian/patches"))
686            .collect::<Vec<_>>();
687        assert_eq!(0, patches.len());
688    }
689}
690
691/// Get the upstream tree with patches applied.
692pub fn upstream_with_applied_patches(
693    tree: breezyshim::workingtree::GenericWorkingTree,
694    patches: Vec<UnifiedPatch>,
695) -> breezyshim::Result<Box<dyn PyTree>> {
696    if let Some(patches_branch) = find_patches_branch(&tree) {
697        Ok(Box::new(patches_branch.basis_tree()?) as Box<dyn PyTree>)
698    } else {
699        let upstream_revision = find_patch_base(&tree).unwrap(); // PatchApplicationBaseNotFound(tree)
700        if patches.is_empty() {
701            Ok(Box::new(Clone::clone(&tree)) as Box<dyn PyTree>)
702        } else {
703            let upstream_tree = tree
704                .branch()
705                .repository()
706                .revision_tree(&upstream_revision)?;
707            Ok(Box::new(AppliedPatches::new(&upstream_tree, patches, None)?) as Box<dyn PyTree>)
708        }
709    }
710}
711
712#[cfg(test)]
713mod upstream_with_applied_patches_tests {
714    const COMMITTER: &str = "Test Suite <test@suite.example.com>";
715    use super::*;
716    use breezyshim::tree::{MutableTree, WorkingTree};
717    use breezyshim::workingtree::GenericWorkingTree;
718    use breezyshim::RevisionId;
719
720    fn setup() -> (tempfile::TempDir, GenericWorkingTree, RevisionId) {
721        let td = tempfile::tempdir().unwrap();
722        let tree = breezyshim::controldir::create_standalone_workingtree(
723            td.path(),
724            &breezyshim::controldir::ControlDirFormat::default(),
725        )
726        .unwrap();
727        std::fs::write(td.path().join("afile"), b"some line\n").unwrap();
728        tree.add(&[std::path::Path::new("afile")]).unwrap();
729        let upstream_revid = tree
730            .build_commit()
731            .message("upstream")
732            .committer(COMMITTER)
733            .commit()
734            .unwrap();
735        tree.mkdir(std::path::Path::new("debian")).unwrap();
736        std::fs::write(
737            td.path().join("debian/changelog"),
738            r#"blah (0.38) unstable; urgency=medium
739
740  * Fix something
741
742 -- Jelmer Vernooij <jelmer@debian.org>  Sat, 19 Oct 2019 15:21:53 +0000
743"#,
744        )
745        .unwrap();
746        tree.add(&[std::path::Path::new("debian/changelog")])
747            .unwrap();
748        tree.mkdir(std::path::Path::new("debian/patches")).unwrap();
749        std::fs::write(td.path().join("debian/patches/series"), "1.patch\n").unwrap();
750        tree.add(&[std::path::Path::new("debian/patches/series")])
751            .unwrap();
752        std::fs::write(
753            td.path().join("debian/patches/1.patch"),
754            r#"--- a/afile
755+++ b/afile
756@@ -1 +1 @@
757-some line
758+another line
759--- /dev/null
760+++ b/newfile
761@@ -0,0 +1 @@
762+new line
763"#,
764        )
765        .unwrap();
766        tree.add(&[std::path::Path::new("debian/patches/1.patch")])
767            .unwrap();
768        std::fs::write(td.path().join("unchangedfile"), b"unchanged\n").unwrap();
769        tree.add(&[std::path::Path::new("unchangedfile")]).unwrap();
770
771        (td, tree, upstream_revid)
772    }
773
774    #[test]
775    fn test_upstream_branch() {
776        let (td, tree, upstream_revid) = setup();
777        tree.branch()
778            .tags()
779            .unwrap()
780            .set_tag("upstream/0.38", &upstream_revid)
781            .unwrap();
782        let tags = tree.branch().tags().unwrap().get_tag_dict().unwrap();
783        assert_eq!(Some(&upstream_revid), tags.get("upstream/0.38"));
784        let patches = super::read_quilt_patches(&tree, std::path::Path::new("debian/patches"))
785            .collect::<Vec<_>>();
786        let t = super::upstream_with_applied_patches(tree, patches).unwrap();
787        assert_eq!(
788            b"another line\n".to_vec(),
789            t.get_file_text(std::path::Path::new("afile")).unwrap()
790        );
791        assert_eq!(
792            b"new line\n".to_vec(),
793            t.get_file_text(std::path::Path::new("newfile")).unwrap()
794        );
795        // TODO(jelmer): PreviewTree appears to be broken
796        // self.assertEqual(b'unchanged\n',
797        //                  t.get_file_text('unchangedfile'))
798        std::mem::drop(td);
799    }
800}
801
802/// Check if a Debian tree has changes vs upstream tree.
803pub fn tree_non_patches_changes(
804    tree: breezyshim::workingtree::GenericWorkingTree,
805    patches_directory: Option<&std::path::Path>,
806) -> breezyshim::Result<Vec<breezyshim::tree::TreeChange>> {
807    let patches = if let Some(patches_directory) = patches_directory.as_ref() {
808        read_quilt_patches(&tree, patches_directory).collect::<Vec<_>>()
809    } else {
810        vec![]
811    };
812
813    let patches_tree = if patches.is_empty() {
814        Box::new(Clone::clone(&tree))
815    } else {
816        Box::new(AppliedPatches::new(&tree, patches.clone(), None)?) as Box<dyn Tree>
817    };
818
819    let upstream_patches_tree = upstream_with_applied_patches(tree, patches)?;
820
821    let changes = patches_tree
822        .iter_changes(upstream_patches_tree.as_ref(), None, None, None)?
823        .map(|c| c.unwrap());
824
825    let paths = &[std::path::Path::new("debian")][..];
826
827    Ok(filter_excluded(changes, paths)
828        .filter_map(|change| {
829            if change.path.1.as_deref() == Some(std::path::Path::new("")) {
830                None
831            } else {
832                Some(change)
833            }
834        })
835        .collect())
836}
837
838#[cfg(test)]
839mod tree_non_patches_changes_tests {
840    const COMMITTER: &str = "Test Suite <test@suite.example.com>";
841    use super::*;
842    use breezyshim::tree::{MutableTree, WorkingTree};
843    use breezyshim::workingtree::GenericWorkingTree;
844    use breezyshim::RevisionId;
845
846    fn setup() -> (tempfile::TempDir, GenericWorkingTree, RevisionId) {
847        breezyshim::init();
848
849        let td = tempfile::tempdir().unwrap();
850        let local_tree = breezyshim::controldir::create_standalone_workingtree(
851            td.path(),
852            &breezyshim::controldir::ControlDirFormat::default(),
853        )
854        .unwrap();
855
856        std::fs::write(td.path().join("afile"), b"some line\n").unwrap();
857        local_tree.add(&[std::path::Path::new("afile")]).unwrap();
858        let upstream_revid = local_tree
859            .build_commit()
860            .message("upstream")
861            .committer(COMMITTER)
862            .commit()
863            .unwrap();
864
865        local_tree.mkdir(std::path::Path::new("debian")).unwrap();
866        std::fs::write(
867            td.path().join("debian/changelog"),
868            r#"blah (0.38) unstable; urgency=medium
869
870  * Fix something
871
872 -- Jelmer Vernooij <jelmer@debian.org>  Sat, 19 Oct 2019 15:21:53 +0000
873"#,
874        )
875        .unwrap();
876        local_tree
877            .mkdir(std::path::Path::new("debian/patches"))
878            .unwrap();
879        std::fs::write(td.path().join("debian/patches/series"), "1.patch\n").unwrap();
880        std::fs::write(
881            td.path().join("debian/patches/1.patch"),
882            r#"--- a/afile
883+++ b/afile
884@@ -1 +1 @@
885-some line
886+another line
887"#,
888        )
889        .unwrap();
890        local_tree
891            .add(&[
892                std::path::Path::new("debian/changelog"),
893                std::path::Path::new("debian/patches"),
894                std::path::Path::new("debian/patches/series"),
895                std::path::Path::new("debian/patches/1.patch"),
896            ])
897            .unwrap();
898
899        (td, local_tree, upstream_revid)
900    }
901
902    #[test]
903    fn test_no_delta() {
904        let (td, tree, upstream_revid) = setup();
905        tree.branch()
906            .tags()
907            .unwrap()
908            .set_tag("upstream/0.38", &upstream_revid)
909            .unwrap();
910        assert_eq!(
911            Vec::<breezyshim::tree::TreeChange>::new(),
912            super::tree_non_patches_changes(tree, Some(std::path::Path::new("debian/patches")))
913                .unwrap()
914        );
915        std::mem::drop(td);
916    }
917
918    #[test]
919    fn test_delta() {
920        let (td, tree, upstream_revid) = setup();
921        tree.branch()
922            .tags()
923            .unwrap()
924            .set_tag("upstream/0.38", &upstream_revid)
925            .unwrap();
926        let tags = tree.branch().tags().unwrap().get_tag_dict().unwrap();
927        assert_eq!(Some(&upstream_revid), tags.get("upstream/0.38"));
928        std::fs::write(tree.basedir().join("anotherfile"), b"blah\n").unwrap();
929        tree.add(&[std::path::Path::new("anotherfile")]).unwrap();
930        assert_eq!(
931            1,
932            super::tree_non_patches_changes(tree, Some(std::path::Path::new("debian/patches")))
933                .unwrap()
934                .len()
935        );
936        std::mem::drop(td);
937    }
938}