lintian_brush/
lib.rs

1use debversion::Version;
2use std::collections::{HashMap, HashSet};
3use std::fs::File;
4use std::io::{BufReader, Read};
5use std::process::Command;
6use std::str::FromStr;
7
8use indicatif::ProgressBar;
9
10use breezyshim::dirty_tracker::DirtyTreeTracker;
11use breezyshim::error::Error;
12use breezyshim::tree::{Tree, TreeChange, WorkingTree};
13use breezyshim::workspace::{check_clean_tree, reset_tree_with_dirty_tracker};
14use breezyshim::RevisionId;
15use debian_analyzer::detect_gbp_dch::{guess_update_changelog, ChangelogBehaviour};
16use debian_analyzer::{
17    add_changelog_entry, apply_or_revert, certainty_sufficient, get_committer, min_certainty,
18    ApplyError, Certainty, ChangelogError,
19};
20use debian_changelog::ChangeLog;
21
22#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
23pub enum PackageType {
24    #[serde(rename = "source")]
25    Source,
26    #[serde(rename = "binary")]
27    Binary,
28}
29
30impl FromStr for PackageType {
31    type Err = String;
32
33    fn from_str(value: &str) -> Result<Self, Self::Err> {
34        match value {
35            "source" => Ok(PackageType::Source),
36            "binary" => Ok(PackageType::Binary),
37            _ => Err(format!("Invalid package type: {}", value)),
38        }
39    }
40}
41
42impl std::fmt::Display for PackageType {
43    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
44        match self {
45            PackageType::Source => write!(f, "source"),
46            PackageType::Binary => write!(f, "binary"),
47        }
48    }
49}
50
51#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
52pub struct LintianIssue {
53    pub package: Option<String>,
54    pub package_type: Option<PackageType>,
55    pub tag: Option<String>,
56    pub info: Option<Vec<String>>,
57}
58
59impl LintianIssue {
60    pub fn json(&self) -> serde_json::Value {
61        serde_json::json!({
62            "package": self.package,
63            "package_type": self.package_type.as_ref().map(|t| t.to_string()),
64            "tag": self.tag,
65            "info": self.info,
66        })
67    }
68
69    pub fn from_json(value: serde_json::Value) -> serde_json::Result<Self> {
70        serde_json::from_value(value)
71    }
72
73    pub fn just_tag(tag: String) -> Self {
74        Self {
75            package: None,
76            package_type: None,
77            tag: Some(tag),
78            info: None,
79        }
80    }
81}
82
83#[derive(Clone, PartialEq, Eq, Debug)]
84pub enum LintianIssueParseError {
85    InvalidPackageType(String),
86}
87
88#[cfg(feature = "python")]
89impl pyo3::FromPyObject<'_> for LintianIssue {
90    fn extract_bound(ob: &pyo3::Bound<pyo3::PyAny>) -> pyo3::PyResult<Self> {
91        use pyo3::prelude::*;
92        let package = ob.getattr("package")?.extract::<Option<String>>()?;
93        let package_type = ob
94            .getattr("package_type")?
95            .extract::<Option<String>>()?
96            .map(|s| {
97                s.parse::<PackageType>()
98                    .map_err(|e| pyo3::exceptions::PyValueError::new_err((e,)))
99            })
100            .transpose()?;
101        let tag = ob.getattr("tag")?.extract::<Option<String>>()?;
102        let info = ob.getattr("info")?.extract::<Option<Vec<String>>>()?;
103        Ok(Self {
104            package,
105            package_type,
106            tag,
107            info,
108        })
109    }
110}
111
112impl std::fmt::Display for LintianIssueParseError {
113    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
114        match self {
115            LintianIssueParseError::InvalidPackageType(s) => {
116                write!(f, "Invalid package type: {}", s)
117            }
118        }
119    }
120}
121
122impl std::error::Error for LintianIssueParseError {}
123
124impl TryFrom<&str> for LintianIssue {
125    type Error = LintianIssueParseError;
126
127    fn try_from(value: &str) -> Result<Self, Self::Error> {
128        let value = value.trim();
129        let package_type;
130        let package;
131        let after = if let Some((before, after)) = value.split_once(':') {
132            if let Some((package_type_str, package_str)) = before.trim().split_once(' ') {
133                package_type = Some(match package_type_str {
134                    "source" => PackageType::Source,
135                    "binary" => PackageType::Binary,
136                    _ => {
137                        return Err(LintianIssueParseError::InvalidPackageType(
138                            package_type_str.to_string(),
139                        ))
140                    }
141                });
142                package = Some(package_str.to_string());
143            } else {
144                package_type = None;
145                package = Some(before.to_string());
146            }
147            after
148        } else {
149            package_type = None;
150            package = None;
151            value
152        };
153        let mut parts = after.trim().split(' ');
154        let tag = parts.next().map(|s| s.to_string());
155        let info: Vec<_> = parts.map(|s| s.to_string()).collect();
156        let info = if info.is_empty() { None } else { Some(info) };
157        Ok(Self {
158            package,
159            package_type,
160            tag,
161            info,
162        })
163    }
164}
165
166#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
167pub struct FixerResult {
168    pub description: String,
169    pub certainty: Option<Certainty>,
170    pub patch_name: Option<String>,
171    pub revision_id: Option<RevisionId>,
172    pub fixed_lintian_issues: Vec<LintianIssue>,
173    pub overridden_lintian_issues: Vec<LintianIssue>,
174}
175
176impl FixerResult {
177    pub fn new(
178        description: String,
179        fixed_lintian_tags: Option<Vec<String>>,
180        certainty: Option<Certainty>,
181        patch_name: Option<String>,
182        revision_id: Option<RevisionId>,
183        mut fixed_lintian_issues: Vec<LintianIssue>,
184        overridden_lintian_issues: Option<Vec<LintianIssue>>,
185    ) -> Self {
186        if let Some(fixed_lintian_tags) = fixed_lintian_tags.as_ref() {
187            fixed_lintian_issues.extend(
188                fixed_lintian_tags
189                    .iter()
190                    .map(|tag| LintianIssue::just_tag(tag.to_string())),
191            );
192        }
193        Self {
194            description,
195            certainty,
196            patch_name,
197            revision_id,
198            fixed_lintian_issues,
199            overridden_lintian_issues: overridden_lintian_issues.unwrap_or_default(),
200        }
201    }
202    pub fn fixed_lintian_tags(&self) -> Vec<&str> {
203        self.fixed_lintian_issues
204            .iter()
205            .filter_map(|issue| issue.tag.as_deref())
206            .collect()
207    }
208}
209
210#[derive(Clone, PartialEq, Eq, Debug)]
211pub enum OutputParseError {
212    UnsupportedCertainty(String),
213    LintianIssueParseError(LintianIssueParseError),
214}
215
216impl std::fmt::Display for OutputParseError {
217    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
218        match self {
219            OutputParseError::UnsupportedCertainty(s) => {
220                write!(f, "Unsupported certainty: {}", s)
221            }
222            OutputParseError::LintianIssueParseError(e) => write!(f, "{}", e),
223        }
224    }
225}
226
227impl std::error::Error for OutputParseError {}
228
229impl From<LintianIssueParseError> for OutputParseError {
230    fn from(value: LintianIssueParseError) -> Self {
231        Self::LintianIssueParseError(value)
232    }
233}
234
235pub fn parse_script_fixer_output(text: &str) -> Result<FixerResult, OutputParseError> {
236    let mut description: Vec<String> = Vec::new();
237    let mut overridden_issues: Vec<LintianIssue> = Vec::new();
238    let mut fixed_lintian_issues: Vec<LintianIssue> = Vec::new();
239    let mut fixed_lintian_tags: Vec<String> = Vec::new();
240    let mut certainty: Option<String> = None;
241    let mut patch_name: Option<String> = None;
242
243    let lines: Vec<&str> = text.split_terminator('\n').collect();
244    let mut i = 0;
245
246    while i < lines.len() {
247        if let Some((key, value)) = lines[i].split_once(':') {
248            match key.trim() {
249                "Fixed-Lintian-Tags" => {
250                    fixed_lintian_tags.extend(value.split(',').map(|tag| tag.trim().to_owned()));
251                }
252                "Fixed-Lintian-Issues" => {
253                    i += 1;
254                    while i < lines.len() && lines[i].starts_with(' ') {
255                        fixed_lintian_issues.push(LintianIssue::try_from(&lines[i][1..])?);
256                        i += 1;
257                    }
258                    continue;
259                }
260                "Overridden-Lintian-Issues" => {
261                    i += 1;
262                    while i < lines.len() && lines[i].starts_with(' ') {
263                        overridden_issues.push(LintianIssue::try_from(&lines[i][1..])?);
264                        i += 1;
265                    }
266                    continue;
267                }
268                "Certainty" => {
269                    certainty = Some(value.trim().to_owned());
270                }
271                "Patch-Name" => {
272                    patch_name = Some(value.trim().to_owned());
273                }
274                _ => {
275                    description.push(lines[i].to_owned());
276                }
277            }
278        } else {
279            description.push(lines[i].to_owned());
280        }
281
282        i += 1;
283    }
284
285    let certainty = certainty
286        .map(|c| c.parse())
287        .transpose()
288        .map_err(OutputParseError::UnsupportedCertainty)?;
289
290    let fixed_lintian_tags = if fixed_lintian_tags.is_empty() {
291        None
292    } else {
293        Some(fixed_lintian_tags)
294    };
295
296    let overridden_issues = if overridden_issues.is_empty() {
297        None
298    } else {
299        Some(overridden_issues)
300    };
301
302    Ok(FixerResult::new(
303        description.join("\n"),
304        fixed_lintian_tags,
305        certainty,
306        patch_name,
307        None,
308        fixed_lintian_issues,
309        overridden_issues,
310    ))
311}
312
313pub fn determine_env(
314    package: &str,
315    current_version: &Version,
316    preferences: &FixerPreferences,
317) -> std::collections::HashMap<String, String> {
318    let mut env = std::env::vars().collect::<std::collections::HashMap<_, _>>();
319    env.insert("DEB_SOURCE".to_owned(), package.to_owned());
320    env.insert("CURRENT_VERSION".to_owned(), current_version.to_string());
321    env.insert(
322        "COMPAT_RELEASE".to_owned(),
323        preferences
324            .compat_release
325            .as_deref()
326            .unwrap_or("sid")
327            .to_owned(),
328    );
329    env.insert(
330        "MINIMUM_CERTAINTY".to_owned(),
331        preferences
332            .minimum_certainty
333            .unwrap_or_default()
334            .to_string(),
335    );
336    env.insert(
337        "TRUST_PACKAGE".to_owned(),
338        preferences.trust_package.unwrap_or(false).to_string(),
339    );
340    env.insert(
341        "REFORMATTING".to_owned(),
342        if preferences.allow_reformatting.unwrap_or(false) {
343            "allow"
344        } else {
345            "disallow"
346        }
347        .to_owned(),
348    );
349    env.insert(
350        "NET_ACCESS".to_owned(),
351        if preferences.net_access.unwrap_or(true) {
352            "allow"
353        } else {
354            "disallow"
355        }
356        .to_owned(),
357    );
358    env.insert(
359        "OPINIONATED".to_owned(),
360        if preferences.opinionated.unwrap_or(false) {
361            "yes"
362        } else {
363            "no"
364        }
365        .to_owned(),
366    );
367    env.insert(
368        "DILIGENCE".to_owned(),
369        preferences.diligence.unwrap_or(0).to_string(),
370    );
371    env
372}
373
374#[derive(Debug, Clone, PartialEq, Eq, Default)]
375pub struct FixerPreferences {
376    pub compat_release: Option<String>,
377    pub minimum_certainty: Option<Certainty>,
378    pub trust_package: Option<bool>,
379    pub allow_reformatting: Option<bool>,
380    pub net_access: Option<bool>,
381    pub opinionated: Option<bool>,
382    pub diligence: Option<i32>,
383}
384
385/// A fixer script
386///
387/// The `lintian_tags attribute contains the name of the lintian tags this fixer addresses.
388pub trait Fixer: std::fmt::Debug + Sync {
389    /// Name of the fixer
390    fn name(&self) -> String;
391
392    /// Path to the fixer script
393    fn path(&self) -> std::path::PathBuf;
394
395    /// Lintian tags this fixer addresses
396    fn lintian_tags(&self) -> Vec<String>;
397
398    /// Apply this fixer script.
399    ///
400    /// # Arguments
401    ///
402    /// * `basedir` - Directory in which to run
403    /// * `package` - Name of the source package
404    /// * `current_version` - The version of the package that is being created or updated
405    /// * `compat_release` - Compatibility level (a Debian release name)
406    /// * `minimum_certainty` - Minimum certainty level
407    /// * `trust_package` - Whether to run code from the package
408    /// * `allow_reformatting` - Allow reformatting of files that are being changed
409    /// * `net_access` - Allow network access
410    /// * `opinionated` - Whether to be opinionated
411    /// * `diligence` - Level of diligence
412    /// * `timeout` - Maximum time to run the fixer
413    ///
414    /// # Returns
415    ///
416    ///  A FixerResult object
417    fn run(
418        &self,
419        basedir: &std::path::Path,
420        package: &str,
421        current_version: &Version,
422        preferences: &FixerPreferences,
423        timeout: Option<chrono::Duration>,
424    ) -> Result<FixerResult, FixerError>;
425}
426
427/// A fixer that is implemented as a Python script.
428///
429/// This gets used just for Python scripts, and significantly speeds things up because it prevents
430/// starting a new Python interpreter for every fixer.
431#[cfg(feature = "python")]
432#[derive(Debug)]
433pub struct PythonScriptFixer {
434    path: std::path::PathBuf,
435    name: String,
436    lintian_tags: Vec<String>,
437}
438
439#[cfg(feature = "python")]
440impl PythonScriptFixer {
441    pub fn new(name: String, lintian_tags: Vec<String>, path: std::path::PathBuf) -> Self {
442        Self {
443            path,
444            name,
445            lintian_tags,
446        }
447    }
448}
449
450#[cfg(feature = "python")]
451fn run_inline_python_fixer(
452    path: &std::path::Path,
453    name: &str,
454    code: &str,
455    basedir: &std::path::Path,
456    env: HashMap<String, String>,
457    _timeout: Option<chrono::Duration>,
458) -> Result<FixerResult, FixerError> {
459    pyo3::prepare_freethreaded_python();
460
461    use pyo3::import_exception;
462    use pyo3::prelude::*;
463    use pyo3::types::PyDict;
464
465    import_exception!(debmutate.reformatting, FormattingUnpreservable);
466    import_exception!(debian.changelog, ChangelogCreateError);
467
468    Python::with_gil(|py| {
469        let sys = py.import_bound("sys")?;
470        let os = py.import_bound("os")?;
471        let io = py.import_bound("io")?;
472        let fixer_module = py.import_bound("lintian_brush.fixer")?;
473
474        let old_env = os.getattr("environ")?.into_py(py);
475        let old_stderr = sys.getattr("stderr")?;
476        let old_stdout = sys.getattr("stdout")?;
477
478        let temp_stderr = io.call_method0("StringIO")?;
479        let temp_stdout = io.call_method0("StringIO")?;
480
481        sys.setattr("stderr", &temp_stderr)?;
482        sys.setattr("stdout", &temp_stdout)?;
483        os.setattr("environ", env)?;
484
485        let old_cwd = match os.call_method0("getcwd") {
486            Ok(cwd) => Some(cwd),
487            Err(_) => None,
488        };
489
490        os.call_method1("chdir", (basedir,))?;
491
492        let global_vars = PyDict::new_bound(py);
493        global_vars.set_item("__file__", path)?;
494        global_vars.set_item("__name__", "__main__")?;
495
496        let script_result = PyModule::from_code_bound(py, code, path.to_str().unwrap(), name);
497
498        let stdout = temp_stdout
499            .call_method0("getvalue")
500            .unwrap()
501            .extract::<String>()
502            .unwrap();
503
504        let mut stderr = temp_stderr
505            .call_method0("getvalue")
506            .unwrap()
507            .extract::<String>()
508            .unwrap();
509
510        os.setattr("environ", old_env).unwrap();
511        sys.setattr("stderr", old_stderr).unwrap();
512        sys.setattr("stdout", old_stdout).unwrap();
513
514        if let Some(cwd) = old_cwd {
515            os.call_method1("chdir", (cwd,))?;
516        }
517
518        fixer_module.call_method0("reset")?;
519
520        let retcode;
521        let description;
522
523        match script_result {
524            Ok(_) => {
525                retcode = 0;
526                description = stdout;
527            }
528            Err(e) => {
529                if e.is_instance_of::<FormattingUnpreservable>(py) {
530                    return Err(FixerError::FormattingUnpreservable(
531                        e.into_value(py).bind(py).getattr("path")?.extract()?,
532                    ));
533                } else if e.is_instance_of::<ChangelogCreateError>(py) {
534                    return Err(FixerError::ChangelogCreate(
535                        e.into_value(py).bind(py).get_item(0)?.extract()?,
536                    ));
537                } else if e.is_instance_of::<pyo3::exceptions::PyMemoryError>(py) {
538                    return Err(FixerError::MemoryError);
539                } else if e.is_instance_of::<pyo3::exceptions::PySystemExit>(py) {
540                    retcode = e.into_value(py).bind(py).getattr("code")?.extract()?;
541                    description = stdout;
542                } else {
543                    use pyo3::types::IntoPyDict;
544                    let traceback = py.import_bound("traceback")?;
545                    let traceback_io = io.call_method0("StringIO")?;
546                    let kwargs = [("file", &traceback_io)].into_py_dict_bound(py);
547                    traceback.call_method(
548                        "print_exception",
549                        (e.get_type_bound(py), &e, e.traceback_bound(py)),
550                        Some(&kwargs),
551                    )?;
552                    let traceback_str =
553                        traceback_io.call_method0("getvalue")?.extract::<String>()?;
554                    stderr = format!("{}\n{}", stderr, traceback_str);
555                    return Err(FixerError::ScriptFailed {
556                        path: path.to_path_buf(),
557                        exit_code: 1,
558                        stderr,
559                    });
560                }
561            }
562        }
563
564        if retcode == 2 {
565            Err(FixerError::NoChanges)
566        } else if retcode != 0 {
567            Err(FixerError::ScriptFailed {
568                path: path.to_path_buf(),
569                exit_code: retcode,
570                stderr,
571            })
572        } else {
573            Ok(parse_script_fixer_output(&description)?)
574        }
575    })
576}
577
578#[cfg(test)]
579#[cfg(feature = "python")]
580mod run_inline_python_fixer_tests {
581    fn setup() {
582        pyo3::Python::with_gil(|py| {
583            use pyo3::prelude::*;
584            let sys = py.import_bound("sys").unwrap();
585            let path = sys.getattr("path").unwrap();
586            let mut path: Vec<String> = path.extract().unwrap();
587            let extra_path =
588                std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR").to_string() + "/../py")
589                    .canonicalize()
590                    .unwrap();
591            if !path.contains(&extra_path.to_string_lossy().to_string()) {
592                path.insert(0, extra_path.to_string_lossy().to_string());
593                sys.setattr("path", path).unwrap();
594            }
595        });
596    }
597
598    #[test]
599    fn test_no_changes() {
600        setup();
601        let td = tempfile::tempdir().unwrap();
602        let path = td.path().join("no_changes.py");
603        let result = super::run_inline_python_fixer(
604            &path,
605            "no_changes",
606            "import sys; sys.exit(2)",
607            td.path(),
608            std::collections::HashMap::new(),
609            None,
610        );
611        assert!(
612            matches!(result, Err(super::FixerError::NoChanges),),
613            "Result: {:?}",
614            result
615        );
616    }
617
618    #[test]
619    fn test_failed() {
620        setup();
621        let td = tempfile::tempdir().unwrap();
622        let path = td.path().join("no_changes.py");
623        let result = super::run_inline_python_fixer(
624            &path,
625            "some_changes",
626            "import sys; sys.exit(1)",
627            td.path(),
628            std::collections::HashMap::new(),
629            None,
630        );
631        assert!(
632            matches!(
633                result,
634                Err(super::FixerError::ScriptFailed { exit_code: 1, .. })
635            ),
636            "Result: {:?}",
637            result
638        );
639        std::mem::drop(td);
640    }
641
642    #[test]
643    #[ignore]
644    fn test_timeout() {
645        setup();
646        let td = tempfile::tempdir().unwrap();
647        let path = td.path().join("no_changes.py");
648        let result = super::run_inline_python_fixer(
649            &path,
650            "some_changes",
651            "import time; time.sleep(10)",
652            td.path(),
653            std::collections::HashMap::new(),
654            Some(chrono::Duration::seconds(0)),
655        );
656        assert!(
657            matches!(result, Err(super::FixerError::Timeout { .. })),
658            "Result: {:?}",
659            result
660        );
661    }
662}
663
664#[cfg(feature = "python")]
665impl Fixer for PythonScriptFixer {
666    fn name(&self) -> String {
667        self.name.clone()
668    }
669
670    fn path(&self) -> std::path::PathBuf {
671        self.path.clone()
672    }
673
674    fn lintian_tags(&self) -> Vec<String> {
675        self.lintian_tags.clone()
676    }
677
678    fn run(
679        &self,
680        basedir: &std::path::Path,
681        package: &str,
682        current_version: &Version,
683        preferences: &FixerPreferences,
684        timeout: Option<chrono::Duration>,
685    ) -> Result<FixerResult, FixerError> {
686        let env = determine_env(package, current_version, preferences);
687
688        let code = std::fs::read_to_string(&self.path)
689            .map_err(|e| FixerError::Other(format!("Failed to read script: {}", e)))?;
690
691        run_inline_python_fixer(
692            &self.path,
693            self.name.as_str(),
694            code.as_str(),
695            basedir,
696            env,
697            timeout,
698        )
699    }
700}
701
702#[derive(Debug)]
703pub enum FixerError {
704    NoChanges,
705    NoChangesAfterOverrides(Vec<LintianIssue>),
706    NotCertainEnough(Certainty, Option<Certainty>, Vec<LintianIssue>),
707    NotDebianPackage(std::path::PathBuf),
708    DescriptionMissing,
709    InvalidChangelog(std::path::PathBuf, String),
710    ScriptNotFound(std::path::PathBuf),
711    OutputParseError(OutputParseError),
712    OutputDecodeError(std::string::FromUtf8Error),
713    FailedPatchManipulation(String),
714    ChangelogCreate(String),
715    Timeout {
716        timeout: chrono::Duration,
717    },
718    ScriptFailed {
719        path: std::path::PathBuf,
720        exit_code: i32,
721        stderr: String,
722    },
723    FormattingUnpreservable(std::path::PathBuf),
724    GeneratedFile(std::path::PathBuf),
725    #[cfg(feature = "python")]
726    Python(pyo3::PyErr),
727    MemoryError,
728    Io(std::io::Error),
729    BrzError(Error),
730    Other(String),
731}
732
733impl From<debian_analyzer::editor::EditorError> for FixerError {
734    fn from(e: debian_analyzer::editor::EditorError) -> Self {
735        match e {
736            debian_analyzer::editor::EditorError::IoError(e) => e.into(),
737            debian_analyzer::editor::EditorError::BrzError(e) => e.into(),
738            debian_analyzer::editor::EditorError::GeneratedFile(p, _) => {
739                FixerError::GeneratedFile(p)
740            }
741            debian_analyzer::editor::EditorError::FormattingUnpreservable(p, _e) => {
742                FixerError::FormattingUnpreservable(p)
743            }
744            debian_analyzer::editor::EditorError::TemplateError(p, _e) => {
745                FixerError::GeneratedFile(p)
746            }
747        }
748    }
749}
750
751impl From<std::io::Error> for FixerError {
752    fn from(e: std::io::Error) -> Self {
753        FixerError::Io(e)
754    }
755}
756
757impl From<debian_changelog::Error> for FixerError {
758    fn from(e: debian_changelog::Error) -> Self {
759        match e {
760            debian_changelog::Error::Io(e) => FixerError::Io(e),
761            debian_changelog::Error::Parse(e) => FixerError::ChangelogCreate(e.to_string()),
762        }
763    }
764}
765
766impl From<ChangelogError> for FixerError {
767    fn from(e: ChangelogError) -> Self {
768        match e {
769            ChangelogError::NotDebianPackage(path) => FixerError::NotDebianPackage(path),
770            ChangelogError::Python(e) => FixerError::Other(e.to_string()),
771        }
772    }
773}
774
775impl From<Error> for FixerError {
776    fn from(e: Error) -> Self {
777        FixerError::BrzError(e)
778    }
779}
780
781impl From<OutputParseError> for FixerError {
782    fn from(e: OutputParseError) -> Self {
783        FixerError::OutputParseError(e)
784    }
785}
786
787#[cfg(feature = "python")]
788impl From<pyo3::PyErr> for FixerError {
789    fn from(e: pyo3::PyErr) -> Self {
790        FixerError::Python(e)
791    }
792}
793
794impl std::fmt::Display for FixerError {
795    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
796        match self {
797            FixerError::NoChanges => write!(f, "No changes"),
798            FixerError::NoChangesAfterOverrides(_) => write!(f, "No changes after overrides"),
799            FixerError::OutputParseError(e) => write!(f, "Output parse error: {}", e),
800            FixerError::OutputDecodeError(e) => write!(f, "Output decode error: {}", e),
801            FixerError::ScriptNotFound(p) => write!(f, "Command not found: {}", p.display()),
802            FixerError::ChangelogCreate(m) => write!(f, "Changelog create error: {}", m),
803            FixerError::FormattingUnpreservable(p) => {
804                write!(f, "Formatting unpreservable for {}", p.display())
805            }
806            FixerError::ScriptFailed {
807                path,
808                exit_code,
809                stderr,
810            } => write!(
811                f,
812                "Script failed: {} (exit code {}) (stderr: {})",
813                path.display(),
814                exit_code,
815                stderr
816            ),
817            FixerError::Other(s) => write!(f, "{}", s),
818            #[cfg(feature = "python")]
819            FixerError::Python(e) => write!(f, "{}", e),
820            FixerError::NotDebianPackage(p) => write!(f, "Not a Debian package: {}", p.display()),
821            FixerError::DescriptionMissing => {
822                write!(f, "Description missing")
823            }
824            FixerError::MemoryError => {
825                write!(f, "Memory error")
826            }
827            FixerError::NotCertainEnough(actual, minimum, _) => write!(
828                f,
829                "Not certain enough to fix (actual: {}, minimum : {:?})",
830                actual, minimum
831            ),
832            FixerError::Io(e) => write!(f, "IO error: {}", e),
833            FixerError::FailedPatchManipulation(s) => write!(f, "Failed to manipulate patc: {}", s),
834            FixerError::BrzError(e) => write!(f, "Breezy error: {}", e),
835            FixerError::InvalidChangelog(p, s) => {
836                write!(f, "Invalid changelog {}: {}", p.display(), s)
837            }
838            FixerError::Timeout { timeout } => write!(f, "Timeout after {}", humantime::format_duration(timeout.to_std().unwrap())),
839            FixerError::GeneratedFile(p) => write!(f, "Generated file: {}", p.display()),
840        }
841    }
842}
843
844impl std::error::Error for FixerError {}
845
846#[derive(Debug)]
847pub struct ScriptFixer {
848    path: std::path::PathBuf,
849    name: String,
850    lintian_tags: Vec<String>,
851}
852
853impl ScriptFixer {
854    pub fn new(name: String, lintian_tags: Vec<String>, path: std::path::PathBuf) -> Self {
855        Self {
856            path,
857            name,
858            lintian_tags,
859        }
860    }
861}
862
863impl Fixer for ScriptFixer {
864    fn name(&self) -> String {
865        self.name.clone()
866    }
867
868    fn path(&self) -> std::path::PathBuf {
869        self.path.clone()
870    }
871
872    fn lintian_tags(&self) -> Vec<String> {
873        self.lintian_tags.clone()
874    }
875
876    fn run(
877        &self,
878        basedir: &std::path::Path,
879        package: &str,
880        current_version: &Version,
881        preferences: &FixerPreferences,
882        timeout: Option<chrono::Duration>,
883    ) -> Result<FixerResult, FixerError> {
884        let env = determine_env(package, current_version, preferences);
885        use wait_timeout::ChildExt;
886
887        let mut cmd = Command::new(self.path.as_os_str());
888        cmd.stdout(std::process::Stdio::piped());
889        cmd.stderr(std::process::Stdio::piped());
890        cmd.current_dir(basedir);
891
892        for (key, value) in env.iter() {
893            cmd.env(key, value);
894        }
895
896        let mut child = cmd.spawn().map_err(|e| match e.kind() {
897            std::io::ErrorKind::NotFound => FixerError::ScriptNotFound(self.path.clone()),
898            _ => FixerError::Other(e.to_string()),
899        })?;
900
901        let status = if let Some(timeout) = timeout {
902            let std_timeout = timeout
903                .to_std()
904                .map_err(|e| FixerError::Other(e.to_string()))?;
905            let output = child
906                .wait_timeout(std_timeout)
907                .map_err(|e| FixerError::Other(e.to_string()))?;
908
909            if output.is_none() {
910                child.kill().map_err(|e| FixerError::Other(e.to_string()))?;
911                return Err(FixerError::Timeout { timeout });
912            }
913            output.unwrap()
914        } else {
915            child.wait().map_err(|e| FixerError::Other(e.to_string()))?
916        };
917
918        if !status.success() {
919            let mut stderr = String::new();
920            let mut stderr_buf = std::io::BufReader::new(child.stderr.as_mut().unwrap());
921            stderr_buf
922                .read_to_string(&mut stderr)
923                .map_err(|e| FixerError::Other(format!("Failed to read stderr: {}", e)))?;
924
925            if status.code() == Some(2) {
926                return Err(FixerError::NoChanges);
927            }
928
929            return Err(FixerError::ScriptFailed {
930                path: self.path.to_owned(),
931                exit_code: status.code().unwrap(),
932                stderr,
933            });
934        }
935
936        let mut stdout_buf = std::io::BufReader::new(child.stdout.as_mut().unwrap());
937
938        let mut stdout = String::new();
939        stdout_buf
940            .read_to_string(&mut stdout)
941            .map_err(FixerError::Io)?;
942        parse_script_fixer_output(&stdout).map_err(FixerError::OutputParseError)
943    }
944}
945
946#[derive(Debug, serde::Deserialize)]
947struct DescEntry {
948    script: String,
949    #[serde(rename = "lintian-tags")]
950    lintian_tags: Option<Vec<String>>,
951    #[serde(rename = "force-subprocess")]
952    force_subprocess: Option<bool>,
953}
954
955#[derive(Debug)]
956pub enum FixerDiscoverError {
957    Io(std::io::Error),
958    Yaml(serde_yaml::Error),
959    NoFixersDir,
960}
961
962impl From<std::io::Error> for FixerDiscoverError {
963    fn from(e: std::io::Error) -> Self {
964        Self::Io(e)
965    }
966}
967
968impl From<serde_yaml::Error> for FixerDiscoverError {
969    fn from(e: serde_yaml::Error) -> Self {
970        Self::Yaml(e)
971    }
972}
973
974impl std::fmt::Display for FixerDiscoverError {
975    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
976        match self {
977            FixerDiscoverError::Io(e) => write!(f, "IO error: {}", e),
978            FixerDiscoverError::Yaml(e) => write!(f, "YAML error: {}", e),
979            FixerDiscoverError::NoFixersDir => write!(f, "No fixers directory found"),
980        }
981    }
982}
983
984impl std::error::Error for FixerDiscoverError {}
985
986pub fn read_desc_file<P: AsRef<std::path::Path>>(
987    path: P,
988    force_subprocess: bool,
989) -> Result<impl Iterator<Item = Box<dyn Fixer>>, FixerDiscoverError> {
990    let file = File::open(path.as_ref())?;
991    let reader = BufReader::new(file);
992
993    let data: Vec<DescEntry> = serde_yaml::from_reader(reader)?;
994
995    let dirname = path.as_ref().parent().unwrap().to_owned();
996    let fixer_iter = data.into_iter().map(move |item| {
997        let script = item.script;
998        let lintian_tags = item.lintian_tags;
999        let force_subprocess = item.force_subprocess.unwrap_or(force_subprocess);
1000        let name = std::path::Path::new(script.as_str())
1001            .file_stem()
1002            .and_then(|name| name.to_str())
1003            .unwrap_or("");
1004        let script_path = dirname.join(script.as_str());
1005
1006        load_fixer(
1007            name.to_owned(),
1008            lintian_tags.unwrap_or_default(),
1009            script_path,
1010            force_subprocess,
1011        )
1012    });
1013
1014    Ok(fixer_iter)
1015}
1016
1017#[cfg(test)]
1018mod read_desc_file_tests {
1019    #[test]
1020    fn test_empty() {
1021        let td = tempfile::tempdir().unwrap();
1022        let path = td.path().join("empty.desc");
1023        std::fs::write(&path, "").unwrap();
1024        assert!(super::read_desc_file(&path, false)
1025            .unwrap()
1026            .next()
1027            .is_none());
1028    }
1029
1030    #[test]
1031    fn test_single() {
1032        let td = tempfile::tempdir().unwrap();
1033        let path = td.path().join("single.desc");
1034        std::fs::write(
1035            &path,
1036            r#"---
1037- script: foo.sh
1038  lintian-tags:
1039  - bar
1040  - baz
1041"#,
1042        )
1043        .unwrap();
1044        let script_path = td.path().join("foo.sh");
1045        std::fs::write(script_path, "#!/bin/sh\n").unwrap();
1046        let fixer = super::read_desc_file(&path, false).unwrap().next().unwrap();
1047        assert_eq!(fixer.name(), "foo");
1048        assert_eq!(fixer.lintian_tags(), vec!["bar", "baz"]);
1049    }
1050}
1051
1052fn load_fixer(
1053    name: String,
1054    tags: Vec<String>,
1055    script_path: std::path::PathBuf,
1056    #[allow(dead_code)] force_subprocess: bool,
1057) -> Box<dyn Fixer> {
1058    #[cfg(feature = "python")]
1059    if script_path
1060        .extension()
1061        .map(|ext| ext == "py")
1062        .unwrap_or(false)
1063        && !force_subprocess
1064    {
1065        return Box::new(PythonScriptFixer::new(name, tags, script_path));
1066    }
1067    Box::new(ScriptFixer::new(name, tags, script_path))
1068}
1069
1070/// Return a list of available lintian fixers.
1071///
1072/// # Arguments
1073///
1074/// * `fixers_dir` - The directory to search for fixers.
1075/// * `force_subprocess` - Force the use of a subprocess for all fixers.
1076pub fn available_lintian_fixers(
1077    fixers_dir: Option<&std::path::Path>,
1078    force_subprocess: Option<bool>,
1079) -> Result<impl Iterator<Item = Box<dyn Fixer>>, FixerDiscoverError> {
1080    let fixers_dir = if let Some(fixers_dir) = fixers_dir {
1081        fixers_dir.to_path_buf()
1082    } else {
1083        let system_path = find_fixers_dir();
1084        if let Some(system_path) = system_path {
1085            system_path
1086        } else {
1087            return Err(FixerDiscoverError::NoFixersDir);
1088        }
1089    };
1090    let mut fixers = Vec::new();
1091    // Scan fixers_dir for .desc files
1092    for entry in std::fs::read_dir(fixers_dir)? {
1093        let entry = entry?;
1094        let path = entry.path();
1095        if path.is_file() && path.extension().map(|ext| ext == "desc").unwrap_or(false) {
1096            let fixer_iter = read_desc_file(&path, force_subprocess.unwrap_or(false))?;
1097            fixers.extend(fixer_iter);
1098        }
1099    }
1100
1101    Ok(fixers.into_iter())
1102}
1103
1104#[derive(Debug, PartialEq, Eq)]
1105pub struct UnknownFixer(pub String);
1106
1107impl std::fmt::Display for UnknownFixer {
1108    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1109        write!(f, "Unknown fixer: {}", self.0)
1110    }
1111}
1112
1113impl std::error::Error for UnknownFixer {}
1114
1115/// """Select fixers by name, from a list.
1116///
1117/// # Arguments
1118///
1119/// * `fixers` - List of Fixer objects
1120/// * `names` - Set of names to select
1121/// * `exclude` - Set of names to exclude
1122pub fn select_fixers(
1123    fixers: Vec<Box<dyn Fixer>>,
1124    names: Option<&[&str]>,
1125    exclude: Option<&[&str]>,
1126) -> Result<Vec<Box<dyn Fixer>>, UnknownFixer> {
1127    let mut select_set = names.map(|names| names.iter().cloned().collect::<HashSet<_>>());
1128    let mut exclude_set = exclude.map(|exclude| exclude.iter().cloned().collect::<HashSet<_>>());
1129    let mut ret = vec![];
1130    for f in fixers.into_iter() {
1131        if let Some(select_set) = select_set.as_mut() {
1132            if !select_set.remove(f.name().as_str()) {
1133                if let Some(exclude_set) = exclude_set.as_mut() {
1134                    exclude_set.remove(f.name().as_str());
1135                }
1136                continue;
1137            }
1138        }
1139        if let Some(exclude_set) = exclude_set.as_mut() {
1140            if exclude_set.remove(f.name().as_str()) {
1141                continue;
1142            }
1143        }
1144        ret.push(f);
1145    }
1146    if let Some(select_set) = select_set.filter(|x| !x.is_empty()) {
1147        Err(UnknownFixer(select_set.iter().next().unwrap().to_string()))
1148    } else if let Some(exclude_set) = exclude_set.filter(|x| !x.is_empty()) {
1149        Err(UnknownFixer(exclude_set.iter().next().unwrap().to_string()))
1150    } else {
1151        Ok(ret)
1152    }
1153}
1154
1155#[cfg(test)]
1156mod select_fixers_tests {
1157    use super::*;
1158
1159    #[derive(Debug)]
1160    struct DummyFixer<'a> {
1161        name: &'a str,
1162        tags: Vec<&'a str>,
1163    }
1164
1165    impl DummyFixer<'_> {
1166        fn new<'a>(name: &'a str, tags: &[&'a str]) -> DummyFixer<'a> {
1167            DummyFixer {
1168                name,
1169                tags: tags.to_vec(),
1170            }
1171        }
1172    }
1173
1174    impl<'a> Fixer for DummyFixer<'a> {
1175        fn name(&self) -> String {
1176            self.name.to_string()
1177        }
1178
1179        fn path(&self) -> std::path::PathBuf {
1180            unimplemented!()
1181        }
1182
1183        fn lintian_tags(&self) -> Vec<String> {
1184            self.tags.iter().map(|s| s.to_string()).collect::<Vec<_>>()
1185        }
1186
1187        fn run(
1188            &self,
1189            _basedir: &std::path::Path,
1190            _package: &str,
1191            _current_version: &Version,
1192            _preferences: &FixerPreferences,
1193            _timeout: Option<chrono::Duration>,
1194        ) -> Result<FixerResult, FixerError> {
1195            unimplemented!()
1196        }
1197    }
1198
1199    #[test]
1200    fn test_exists() {
1201        assert_eq!(
1202            Ok(vec!["dummy1".to_string()]),
1203            select_fixers(
1204                vec![
1205                    Box::new(DummyFixer::new("dummy1", &["some-tag"])),
1206                    Box::new(DummyFixer::new("dummy2", &["other-tag"])),
1207                ],
1208                Some(vec!["dummy1"].as_slice()),
1209                None
1210            )
1211            .map(|m| m.into_iter().map(|f| f.name()).collect::<Vec<_>>())
1212        );
1213    }
1214
1215    #[test]
1216    fn test_missing() {
1217        assert!(select_fixers(
1218            vec![
1219                Box::new(DummyFixer::new("dummy1", &["some-tag"])),
1220                Box::new(DummyFixer::new("dummy2", &["other-tag"])),
1221            ],
1222            Some(vec!["other"].as_slice()),
1223            None
1224        )
1225        .is_err());
1226    }
1227
1228    #[test]
1229    fn test_exclude_missing() {
1230        assert!(select_fixers(
1231            vec![
1232                Box::new(DummyFixer::new("dummy1", &["some-tag"])),
1233                Box::new(DummyFixer::new("dummy2", &["other-tag"])),
1234            ],
1235            Some(vec!["dummy"].as_slice()),
1236            Some(vec!["some-other"].as_slice())
1237        )
1238        .is_err());
1239    }
1240
1241    #[test]
1242    fn test_exclude() {
1243        assert_eq!(
1244            Ok(vec!["dummy1".to_string()]),
1245            select_fixers(
1246                vec![
1247                    Box::new(DummyFixer::new("dummy1", &["some-tag"])),
1248                    Box::new(DummyFixer::new("dummy2", &["other-tag"])),
1249                ],
1250                Some(vec!["dummy1"].as_slice()),
1251                Some(vec!["dummy2"].as_slice())
1252            )
1253            .map(|m| m.into_iter().map(|f| f.name()).collect::<Vec<_>>())
1254        );
1255    }
1256}
1257
1258pub const DEFAULT_VALUE_LINTIAN_BRUSH_ADDON_ONLY: i32 = 10;
1259pub const DEFAULT_VALUE_LINTIAN_BRUSH: i32 = 50;
1260pub const LINTIAN_BRUSH_TAG_VALUES: [(&str, i32); 1] = [("trailing-whitespace", 0)];
1261pub const DEFAULT_ADDON_FIXERS: &[&str] = &[
1262    "debian-changelog-line-too-long",
1263    "trailing-whitespace",
1264    "out-of-date-standards-version",
1265    "package-uses-old-debhelper-compat-version",
1266    "public-upstream-key-not-minimal",
1267];
1268pub const LINTIAN_BRUSH_TAG_DEFAULT_VALUE: i32 = 5;
1269
1270pub fn calculate_value(tags: &[&str]) -> i32 {
1271    if tags.is_empty() {
1272        return 0;
1273    }
1274
1275    let default_addon_fixers: HashSet<&str> = DEFAULT_ADDON_FIXERS.iter().cloned().collect();
1276    let tag_set: HashSet<&str> = tags.iter().cloned().collect();
1277
1278    if tag_set.is_subset(&default_addon_fixers) {
1279        return DEFAULT_VALUE_LINTIAN_BRUSH_ADDON_ONLY;
1280    }
1281
1282    let mut value = DEFAULT_VALUE_LINTIAN_BRUSH;
1283
1284    for tag in tags {
1285        if let Some(tag_value) = LINTIAN_BRUSH_TAG_VALUES.iter().find(|(t, _)| t == tag) {
1286            value += tag_value.1;
1287        } else {
1288            value += LINTIAN_BRUSH_TAG_DEFAULT_VALUE;
1289        }
1290    }
1291
1292    value
1293}
1294
1295pub fn data_file_path(
1296    name: &str,
1297    check: impl Fn(&std::path::Path) -> bool,
1298) -> Option<std::path::PathBuf> {
1299    let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1300    path = path.join("..").join(name);
1301    if check(&path) {
1302        return Some(path);
1303    }
1304
1305    #[cfg(feature = "python")]
1306    pyo3::prepare_freethreaded_python();
1307    #[cfg(feature = "python")]
1308    if let Some(path) = pyo3::Python::with_gil(|py| {
1309        use pyo3::prelude::*;
1310        let pkg_resources = py.import_bound("pkg_resources").unwrap();
1311        if let Ok(path) = pkg_resources.call_method1(
1312            "resource_filename",
1313            ("lintian_brush", format!("lintian-brush/{}", name)),
1314        ) {
1315            if let Ok(path) = path.extract::<std::path::PathBuf>() {
1316                if check(path.as_path()) {
1317                    return Some(path);
1318                }
1319            }
1320        }
1321        None
1322    }) {
1323        return Some(path);
1324    }
1325
1326    let base_paths = &["/usr/share/lintian-brush", "/usr/local/share/lintian-brush"];
1327
1328    for base_path in base_paths {
1329        let path = std::path::Path::new(base_path).join(name);
1330        if check(&path) {
1331            return Some(path);
1332        }
1333    }
1334
1335    None
1336}
1337
1338pub fn find_fixers_dir() -> Option<std::path::PathBuf> {
1339    data_file_path("fixers", |path| path.is_dir())
1340}
1341
1342/// Run a lintian fixer on a tree.
1343///
1344/// # Arguments
1345///
1346///  * `local_tree`: WorkingTree object
1347///  * `basis_tree`: Tree
1348///  * `fixer`: Fixer object to apply
1349///  * `committer`: Optional committer (name and email)
1350///  * `update_changelog`: Whether to add a new entry to the changelog
1351///  * `compat_release`: Minimum release that the package should be usable on
1352///  * `  (e.g. 'stable' or 'unstable')
1353///  * `minimum_certainty`: How certain the fixer should be
1354///  * `  about its changes.
1355///  * `trust_package`: Whether to run code from the package if necessary
1356///  * `allow_reformatting`: Whether to allow reformatting of changed files
1357///  * `dirty_tracker`: Optional object that can be used to tell if the tree
1358///  * `  has been changed.
1359///  * `subpath`: Path in tree to operate on
1360///  * `net_access`: Whether to allow accessing external services
1361///  * `opinionated`: Whether to be opinionated
1362///  * `diligence`: Level of diligence
1363///
1364/// # Returns
1365///   tuple with set of FixerResult, summary of the changes
1366pub fn run_lintian_fixer(
1367    local_tree: &WorkingTree,
1368    fixer: &dyn Fixer,
1369    committer: Option<&str>,
1370    mut update_changelog: impl FnMut() -> bool,
1371    preferences: &FixerPreferences,
1372    dirty_tracker: &mut Option<DirtyTreeTracker>,
1373    subpath: &std::path::Path,
1374    timestamp: Option<chrono::naive::NaiveDateTime>,
1375    basis_tree: Option<&dyn Tree>,
1376    changes_by: Option<&str>,
1377    timeout: Option<chrono::Duration>,
1378) -> Result<(FixerResult, String), FixerError> {
1379    let changes_by = changes_by.unwrap_or("lintian-brush");
1380
1381    let changelog_path = subpath.join("debian/changelog");
1382
1383    let r = match local_tree.get_file(changelog_path.as_path()) {
1384        Ok(f) => f,
1385        Err(Error::NoSuchFile(_pb)) => {
1386            return Err(FixerError::NotDebianPackage(
1387                local_tree.abspath(subpath).unwrap(),
1388            ));
1389        }
1390        Err(e) => return Err(FixerError::Other(e.to_string())),
1391    };
1392
1393    let cl = ChangeLog::read(r)?;
1394    let first_entry = if let Some(entry) = cl.entries().next() {
1395        entry
1396    } else {
1397        return Err(FixerError::InvalidChangelog(
1398            local_tree.abspath(subpath).unwrap(),
1399            "No entries in changelog".to_string(),
1400        ));
1401    };
1402    let package = first_entry.package().unwrap();
1403    let current_version: Version =
1404        if first_entry.distributions().as_deref().unwrap() == vec!["UNRELEASED"] {
1405            first_entry.version().unwrap()
1406        } else {
1407            let mut version = first_entry.version().unwrap();
1408            version.increment_debian();
1409            version
1410        };
1411
1412    let mut _bt = None;
1413    let basis_tree: &dyn Tree = if let Some(basis_tree) = basis_tree {
1414        basis_tree
1415    } else {
1416        _bt = Some(local_tree.basis_tree().unwrap());
1417        _bt.as_ref().unwrap()
1418    };
1419
1420    let make_changes = |basedir: &std::path::Path| -> Result<_, FixerError> {
1421        log::debug!("Running fixer {:?}", fixer);
1422        let result = fixer.run(
1423            basedir,
1424            package.as_str(),
1425            &current_version,
1426            preferences,
1427            timeout,
1428        )?;
1429        if let Some(certainty) = result.certainty {
1430            if !certainty_sufficient(certainty, preferences.minimum_certainty) {
1431                return Err(FixerError::NotCertainEnough(
1432                    certainty,
1433                    preferences.minimum_certainty,
1434                    result.overridden_lintian_issues,
1435                ));
1436            }
1437        }
1438
1439        Ok(result)
1440    };
1441
1442    let (mut result, changes, mut specific_files) = match apply_or_revert(
1443        local_tree,
1444        subpath,
1445        basis_tree,
1446        dirty_tracker.as_mut(),
1447        make_changes,
1448    ) {
1449        Ok(r) => {
1450            if r.0.description.is_empty() {
1451                return Err(FixerError::DescriptionMissing);
1452            }
1453
1454            r
1455        }
1456        Err(ApplyError::NoChanges(r)) => {
1457            if r.overridden_lintian_issues.is_empty() {
1458                return Err(FixerError::NoChanges);
1459            } else {
1460                return Err(FixerError::NoChangesAfterOverrides(
1461                    r.overridden_lintian_issues,
1462                ));
1463            }
1464        }
1465        Err(ApplyError::BrzError(e)) => {
1466            return Err(e.into());
1467        }
1468        Err(ApplyError::CallbackError(e)) => {
1469            return Err(e);
1470        }
1471    };
1472
1473    let lines = result.description.split('\n').collect::<Vec<_>>();
1474    let mut summary = lines[0].to_string();
1475    let details = lines
1476        .iter()
1477        .skip(1)
1478        .take_while(|l| !l.is_empty())
1479        .collect::<Vec<_>>();
1480
1481    // If there are upstream changes in a non-native package, perhaps
1482    // export them to debian/patches
1483    if has_non_debian_changes(changes.as_slice(), subpath)
1484        && current_version.debian_revision.is_some()
1485    {
1486        let (patch_name, updated_specific_files) = match upstream_changes_to_patch(
1487            local_tree,
1488            basis_tree,
1489            dirty_tracker.as_mut(),
1490            subpath,
1491            &result
1492                .patch_name
1493                .as_deref()
1494                .map_or_else(|| fixer.name(), |n| n.to_string()),
1495            result.description.as_str(),
1496            timestamp.map(|t| t.date()),
1497        ) {
1498            Ok(r) => r,
1499            Err(e) => {
1500                reset_tree_with_dirty_tracker(
1501                    local_tree,
1502                    Some(basis_tree),
1503                    Some(subpath),
1504                    dirty_tracker.as_mut(),
1505                )
1506                .map_err(|e| FixerError::Other(e.to_string()))?;
1507
1508                return Err(FixerError::FailedPatchManipulation(e.to_string()));
1509            }
1510        };
1511
1512        specific_files = Some(updated_specific_files);
1513
1514        summary = format!("Add patch {}: {}", patch_name, summary);
1515    }
1516
1517    let update_changelog = if debian_analyzer::changelog::only_changes_last_changelog_block(
1518        local_tree,
1519        basis_tree,
1520        changelog_path.as_path(),
1521        changes.iter(),
1522    )? {
1523        // If the script only changed the last entry in the changelog,
1524        // don't update the changelog
1525        false
1526    } else {
1527        update_changelog()
1528    };
1529
1530    if update_changelog {
1531        let mut entry = vec![summary.as_str()];
1532        entry.extend(details);
1533
1534        add_changelog_entry(local_tree, changelog_path.as_path(), entry.as_slice())?;
1535        if let Some(specific_files) = specific_files.as_mut() {
1536            specific_files.push(changelog_path);
1537        }
1538    }
1539
1540    let mut description = format!("{}\n", result.description);
1541    description.push('\n');
1542    description.push_str(format!("Changes-By: {}\n", changes_by).as_str());
1543    for tag in result.fixed_lintian_tags() {
1544        description.push_str(format!("Fixes: lintian: {}\n", tag).as_str());
1545        description
1546            .push_str(format!("See-also: https://lintian.debian.org/tags/{}.html\n", tag).as_str());
1547    }
1548
1549    let committer = committer.map_or_else(|| get_committer(local_tree), |c| c.to_string());
1550
1551    let specific_files_ref = specific_files
1552        .as_ref()
1553        .map(|fs| fs.iter().map(|p| p.as_path()).collect::<Vec<_>>());
1554
1555    let mut builder = local_tree
1556        .build_commit()
1557        .message(description.as_str())
1558        .allow_pointless(false)
1559        .committer(committer.as_str());
1560
1561    if let Some(specific_files_ref) = specific_files_ref.as_ref() {
1562        builder = builder.specific_files(specific_files_ref);
1563    }
1564
1565    let revid = builder.commit().map_err(|e| match e {
1566        Error::PointlessCommit => FixerError::NoChanges,
1567        Error::NoWhoami => FixerError::Other("No committer specified".to_string()),
1568        e => FixerError::Other(e.to_string()),
1569    })?;
1570    result.revision_id = Some(revid);
1571
1572    // TODO(jelmer): Support running sbuild & verify lintian warning is gone?
1573    Ok((result, summary))
1574}
1575
1576#[derive(Debug)]
1577pub enum OverallError {
1578    NotDebianPackage(std::path::PathBuf),
1579    WorkspaceDirty(std::path::PathBuf),
1580    ChangelogCreate(String),
1581    InvalidChangelog(std::path::PathBuf, String),
1582    BrzError(Error),
1583    IoError(std::io::Error),
1584    Other(String),
1585    #[cfg(feature = "python")]
1586    Python(pyo3::PyErr),
1587}
1588
1589#[cfg(feature = "python")]
1590impl From<pyo3::PyErr> for OverallError {
1591    fn from(e: pyo3::PyErr) -> Self {
1592        OverallError::Python(e)
1593    }
1594}
1595
1596impl std::fmt::Display for OverallError {
1597    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1598        match self {
1599            OverallError::NotDebianPackage(path) => {
1600                write!(f, "Not a Debian package: {}", path.display())
1601            }
1602            OverallError::WorkspaceDirty(path) => {
1603                write!(f, "Workspace is dirty: {}", path.display())
1604            }
1605            OverallError::ChangelogCreate(m) => {
1606                write!(f, "Failed to create changelog entry: {}", m)
1607            }
1608            #[cfg(feature = "python")]
1609            OverallError::Python(e) => write!(f, "{}", e),
1610            OverallError::Other(e) => write!(f, "{}", e),
1611            OverallError::BrzError(e) => write!(f, "{}", e),
1612            OverallError::IoError(e) => write!(f, "{}", e),
1613            OverallError::InvalidChangelog(path, e) => {
1614                write!(f, "Invalid changelog at {}: {}", path.display(), e)
1615            }
1616        }
1617    }
1618}
1619
1620impl std::error::Error for OverallError {}
1621
1622/// Run a set of lintian fixers on a tree.
1623///
1624/// # Arguments
1625///
1626///  * `tree`: The tree to run the fixers on
1627///  * `fixers`: A set of Fixer objects
1628///  * `update_changelog`: Whether to add an entry to the changelog
1629///  * `verbose`: Whether to be verbose
1630///  * `committer`: Optional committer (name and email)
1631///  * `compat_release`: Minimum release that the package should be usable on
1632///       (e.g. 'sid' or 'stretch')
1633///  * `minimum_certainty`: How certain the fixer should be about its changes.
1634///  * `trust_package`: Whether to run code from the package if necessary
1635///  * `allow_reformatting`: Whether to allow reformatting of changed files
1636///  * `use_inotify`: Use inotify to watch changes (significantly improves
1637///       performance). Defaults to None (automatic)
1638///  * `subpath`: Subpath in the tree in which the package lives
1639///  * `net_access`: Whether to allow network access
1640///  * `opinionated`: Whether to be opinionated
1641///  * `diligence`: Level of diligence
1642///  * `changes_by`: Name of the person making the changes
1643///  * `timeout`: Per-fixer timeout
1644///
1645/// # Returns:
1646///   Tuple with two lists:
1647///     1. list of tuples with (lintian-tag, certainty, description) of fixers
1648///        that ran
1649///     2. dictionary mapping fixer names for fixers that failed to run to the
1650///        error that occurred
1651pub fn run_lintian_fixers(
1652    local_tree: &WorkingTree,
1653    fixers: &[Box<dyn Fixer>],
1654    mut update_changelog: Option<impl FnMut() -> bool>,
1655    verbose: bool,
1656    committer: Option<&str>,
1657    preferences: &FixerPreferences,
1658    use_dirty_tracker: Option<bool>,
1659    subpath: Option<&std::path::Path>,
1660    changes_by: Option<&str>,
1661    timeout: Option<chrono::Duration>,
1662) -> Result<ManyResult, OverallError> {
1663    let subpath = subpath.unwrap_or_else(|| std::path::Path::new(""));
1664    let mut basis_tree = local_tree.basis_tree().unwrap();
1665    check_clean_tree(local_tree, &basis_tree, subpath).map_err(|e| match e {
1666        Error::WorkspaceDirty(p) => OverallError::WorkspaceDirty(p),
1667        e => OverallError::Other(e.to_string()),
1668    })?;
1669
1670    let mut changelog_behaviour = None;
1671
1672    // If we don't know whether to update the changelog, then find out *once*
1673    let mut update_changelog = || {
1674        if let Some(update_changelog) = update_changelog.as_mut() {
1675            return update_changelog();
1676        }
1677        let debian_path = subpath.join("debian");
1678        let cb = determine_update_changelog(local_tree, debian_path.as_path());
1679        changelog_behaviour = Some(cb);
1680        changelog_behaviour.as_ref().unwrap().update_changelog
1681    };
1682
1683    let mut ret = ManyResult::new();
1684    let pb = ProgressBar::new(fixers.len() as u64);
1685    #[cfg(test)]
1686    pb.set_draw_target(indicatif::ProgressDrawTarget::hidden());
1687    let mut dirty_tracker = if use_dirty_tracker.unwrap_or(true) {
1688        Some(DirtyTreeTracker::new_in_subpath(
1689            local_tree.clone(),
1690            subpath,
1691        ))
1692    } else {
1693        None
1694    };
1695    for fixer in fixers {
1696        pb.set_message(format!("Running fixer {}", fixer.name()));
1697        // Get now from chrono
1698        let start = std::time::SystemTime::now();
1699        if let Some(dirty_tracker) = dirty_tracker.as_mut() {
1700            dirty_tracker.mark_clean();
1701        }
1702        pb.inc(1);
1703        match run_lintian_fixer(
1704            local_tree,
1705            fixer.as_ref(),
1706            committer,
1707            &mut update_changelog,
1708            preferences,
1709            &mut dirty_tracker,
1710            subpath,
1711            None,
1712            Some(&basis_tree),
1713            changes_by,
1714            timeout,
1715        ) {
1716            Err(e) => match e {
1717                FixerError::NotDebianPackage(path) => {
1718                    return Err(OverallError::NotDebianPackage(path));
1719                }
1720                FixerError::ChangelogCreate(m) => {
1721                    return Err(OverallError::ChangelogCreate(m));
1722                }
1723                FixerError::OutputParseError(ref _e) => {
1724                    ret.failed_fixers.insert(fixer.name(), e.to_string());
1725                    if verbose {
1726                        log::info!("Fixer {} failed to parse output.", fixer.name());
1727                    }
1728                    continue;
1729                }
1730                FixerError::DescriptionMissing => {
1731                    ret.failed_fixers.insert(fixer.name(), e.to_string());
1732                    if verbose {
1733                        log::info!(
1734                            "Fixer {} failed because description is missing.",
1735                            fixer.name()
1736                        );
1737                    }
1738                    continue;
1739                }
1740                FixerError::OutputDecodeError(ref _e) => {
1741                    ret.failed_fixers.insert(fixer.name(), e.to_string());
1742                    if verbose {
1743                        log::info!("Fixer {} failed to decode output.", fixer.name());
1744                    }
1745                    continue;
1746                }
1747                FixerError::FormattingUnpreservable(path) => {
1748                    ret.formatting_unpreservable
1749                        .insert(fixer.name(), path.clone());
1750                    if verbose {
1751                        log::info!(
1752                            "Fixer {} was unable to preserve formatting of {}.",
1753                            fixer.name(),
1754                            path.display()
1755                        );
1756                    }
1757                    continue;
1758                }
1759                FixerError::GeneratedFile(p) => {
1760                    ret.failed_fixers
1761                        .insert(fixer.name(), format!("Generated file: {}", p.display()));
1762                    if verbose {
1763                        log::info!(
1764                            "Fixer {} encountered generated file {}",
1765                            fixer.name(),
1766                            p.display()
1767                        );
1768                    }
1769                }
1770                FixerError::ScriptNotFound(ref p) => {
1771                    ret.failed_fixers.insert(fixer.name(), e.to_string());
1772                    if verbose {
1773                        log::info!("Fixer {} ({}) not found.", fixer.name(), p.display());
1774                    }
1775                    continue;
1776                }
1777                FixerError::ScriptFailed { .. } => {
1778                    ret.failed_fixers.insert(fixer.name(), e.to_string());
1779                    if verbose {
1780                        log::info!("Fixer {} failed to run.", fixer.name());
1781                        eprintln!("{}", e);
1782                    }
1783                    continue;
1784                }
1785                FixerError::MemoryError => {
1786                    ret.failed_fixers.insert(fixer.name(), e.to_string());
1787                    if verbose {
1788                        log::info!("Ran out of memory while running fixer {}.", fixer.name());
1789                    }
1790                    continue;
1791                }
1792                FixerError::BrzError(e) => {
1793                    return Err(OverallError::BrzError(e));
1794                }
1795                FixerError::Io(e) => {
1796                    return Err(OverallError::IoError(e));
1797                }
1798                FixerError::NotCertainEnough(actual_certainty, minimum_certainty, _overrides) => {
1799                    if verbose {
1800                        let duration = std::time::SystemTime::now().duration_since(start).unwrap();
1801                        log::info!(
1802                    "Fixer {} made changes but not high enough certainty (was {}, needed {}). (took: {:2}s)",
1803                    fixer.name(),
1804                    actual_certainty,
1805                    minimum_certainty.map_or("default".to_string(), |c| c.to_string()),
1806                    duration.as_secs_f32(),
1807                );
1808                    }
1809                    continue;
1810                }
1811                FixerError::FailedPatchManipulation(ref reason) => {
1812                    if verbose {
1813                        log::info!("Unable to manipulate upstream patches: {}", reason);
1814                    }
1815                    ret.failed_fixers.insert(fixer.name(), e.to_string());
1816                    continue;
1817                }
1818                FixerError::NoChanges => {
1819                    if verbose {
1820                        let duration = std::time::SystemTime::now().duration_since(start).unwrap();
1821                        log::info!(
1822                            "Fixer {} made no changes. (took: {:2}s)",
1823                            fixer.name(),
1824                            duration.as_secs_f32(),
1825                        );
1826                    }
1827                    continue;
1828                }
1829                FixerError::NoChangesAfterOverrides(os) => {
1830                    if verbose {
1831                        let duration = std::time::SystemTime::now().duration_since(start).unwrap();
1832                        log::info!(
1833                            "Fixer {} made no changes. (took: {:2}s)",
1834                            fixer.name(),
1835                            duration.as_secs_f32(),
1836                        );
1837                    }
1838                    ret.overridden_lintian_issues.extend(os);
1839                    continue;
1840                }
1841                #[cfg(feature = "python")]
1842                FixerError::Python(ref ep) => {
1843                    if verbose {
1844                        log::info!("Fixer {} failed: {}", fixer.name(), ep);
1845                    }
1846                    ret.failed_fixers.insert(fixer.name(), e.to_string());
1847                    continue;
1848                }
1849                FixerError::Other(ref em) => {
1850                    if verbose {
1851                        log::info!("Fixer {} failed: {}", fixer.name(), em);
1852                    }
1853                    ret.failed_fixers.insert(fixer.name(), e.to_string());
1854                    continue;
1855                }
1856                FixerError::InvalidChangelog(path, reason) => {
1857                    return Err(OverallError::InvalidChangelog(path, reason));
1858                }
1859                FixerError::Timeout { timeout } => {
1860                    if verbose {
1861                        log::info!("Fixer {} timed out after {}.", fixer.name(), timeout);
1862                    }
1863                    ret.failed_fixers.insert(fixer.name(), e.to_string());
1864                    continue;
1865                }
1866            },
1867            Ok((result, summary)) => {
1868                if verbose {
1869                    let duration = std::time::SystemTime::now().duration_since(start).unwrap();
1870                    log::info!(
1871                        "Fixer {} made changes. (took {:2}s)",
1872                        fixer.name(),
1873                        duration.as_secs_f32(),
1874                    );
1875                }
1876                ret.success.push((result, summary));
1877                basis_tree = local_tree.basis_tree().unwrap();
1878            }
1879        }
1880    }
1881    pb.finish();
1882    ret.changelog_behaviour = changelog_behaviour;
1883    Ok(ret)
1884}
1885
1886#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
1887pub struct ManyResult {
1888    #[serde(rename = "applied")]
1889    pub success: Vec<(FixerResult, String)>,
1890    #[serde(rename = "failed")]
1891    pub failed_fixers: std::collections::HashMap<String, String>,
1892    pub changelog_behaviour: Option<ChangelogBehaviour>,
1893    #[serde(skip)]
1894    pub overridden_lintian_issues: Vec<LintianIssue>,
1895    #[serde(skip)]
1896    pub formatting_unpreservable: std::collections::HashMap<String, std::path::PathBuf>,
1897}
1898
1899impl ManyResult {
1900    pub fn tags_count(&self) -> HashMap<&str, u32> {
1901        self.success
1902            .iter()
1903            .fold(HashMap::new(), |mut acc, (r, _summary)| {
1904                for tag in r.fixed_lintian_tags() {
1905                    *acc.entry(tag).or_insert(0) += 1;
1906                }
1907                acc
1908            })
1909    }
1910
1911    pub fn value(&self) -> i32 {
1912        let tags = self
1913            .success
1914            .iter()
1915            .flat_map(|(r, _summary)| r.fixed_lintian_tags())
1916            .collect::<Vec<_>>();
1917        calculate_value(tags.as_slice())
1918    }
1919
1920    /// Return the minimum certainty of any successfully made change.
1921    pub fn minimum_success_certainty(&self) -> Certainty {
1922        min_certainty(
1923            self.success
1924                .iter()
1925                .filter_map(|(r, _summary)| r.certainty)
1926                .collect::<Vec<_>>()
1927                .as_slice(),
1928        )
1929        .unwrap_or(Certainty::Certain)
1930    }
1931
1932    pub fn new() -> Self {
1933        Self {
1934            success: Vec::new(),
1935            failed_fixers: std::collections::HashMap::new(),
1936            changelog_behaviour: None,
1937            overridden_lintian_issues: Vec::new(),
1938            formatting_unpreservable: std::collections::HashMap::new(),
1939        }
1940    }
1941}
1942
1943fn has_non_debian_changes(changes: &[TreeChange], subpath: &std::path::Path) -> bool {
1944    let debian_path = subpath.join("debian");
1945    changes.iter().any(|change| {
1946        [change.path.0.as_deref(), change.path.1.as_deref()]
1947            .into_iter()
1948            .flatten()
1949            .any(|path| !path.starts_with(&debian_path))
1950    })
1951}
1952
1953#[derive(Debug)]
1954struct FailedPatchManipulation(String);
1955
1956impl std::fmt::Display for FailedPatchManipulation {
1957    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1958        write!(f, "Failed to manipulate patches: {}", self.0)
1959    }
1960}
1961
1962impl std::error::Error for FailedPatchManipulation {}
1963
1964fn upstream_changes_to_patch(
1965    local_tree: &WorkingTree,
1966    basis_tree: &dyn Tree,
1967    dirty_tracker: Option<&mut DirtyTreeTracker>,
1968    subpath: &std::path::Path,
1969    patch_name: &str,
1970    description: &str,
1971    timestamp: Option<chrono::naive::NaiveDate>,
1972) -> Result<(String, Vec<std::path::PathBuf>), FailedPatchManipulation> {
1973    use debian_analyzer::patches::{
1974        move_upstream_changes_to_patch, read_quilt_patches, tree_patches_directory,
1975    };
1976
1977    // TODO(jelmer): Apply all patches before generating a diff.
1978
1979    let patches_directory = tree_patches_directory(local_tree, subpath);
1980    let quilt_patches =
1981        read_quilt_patches(local_tree, patches_directory.as_path()).collect::<Vec<_>>();
1982    if !quilt_patches.is_empty() {
1983        return Err(FailedPatchManipulation(
1984            "Creating patch on top of existing quilt patches not supported.".to_string(),
1985        ));
1986    }
1987
1988    log::debug!("Moving upstream changes to patch {}", patch_name);
1989    let (specific_files, patch_name) = match move_upstream_changes_to_patch(
1990        local_tree,
1991        basis_tree,
1992        subpath,
1993        patch_name,
1994        description,
1995        dirty_tracker,
1996        timestamp,
1997    ) {
1998        Ok(r) => r,
1999        Err(e) => {
2000            return Err(FailedPatchManipulation(e.to_string()));
2001        }
2002    };
2003
2004    Ok((patch_name, specific_files))
2005}
2006
2007fn note_changelog_policy(policy: bool, msg: &str) {
2008    lazy_static::lazy_static! {
2009        static ref CHANGELOG_POLICY_NOTED: std::sync::Mutex<bool> = std::sync::Mutex::new(false);
2010    }
2011    if let Ok(mut policy_noted) = CHANGELOG_POLICY_NOTED.lock() {
2012        if !*policy_noted {
2013            let extra = if policy {
2014                "Specify --no-update-changelog to override."
2015            } else {
2016                "Specify --update-changelog to override."
2017            };
2018            log::info!("{} {}", msg, extra);
2019        }
2020        *policy_noted = true;
2021    }
2022}
2023
2024pub fn determine_update_changelog(
2025    local_tree: &WorkingTree,
2026    debian_path: &std::path::Path,
2027) -> ChangelogBehaviour {
2028    let changelog_path = debian_path.join("changelog");
2029
2030    let cl = match local_tree.get_file(changelog_path.as_path()) {
2031        Ok(f) => ChangeLog::read(f).unwrap(),
2032
2033        Err(Error::NoSuchFile(_)) => {
2034            // If there's no changelog, then there's nothing to update!
2035            return ChangelogBehaviour {
2036                update_changelog: false,
2037                explanation: "No changelog found".to_string(),
2038            };
2039        }
2040        Err(e) => {
2041            panic!("Error reading changelog: {}", e);
2042        }
2043    };
2044
2045    let behaviour = guess_update_changelog(local_tree, debian_path, Some(cl));
2046
2047    let behaviour = if let Some(behaviour) = behaviour {
2048        note_changelog_policy(behaviour.update_changelog, behaviour.explanation.as_str());
2049        behaviour
2050    } else {
2051        // If we can't make an educated guess, assume yes.
2052        ChangelogBehaviour {
2053            update_changelog: true,
2054            explanation: "Assuming changelog should be updated".to_string(),
2055        }
2056    };
2057
2058    behaviour
2059}
2060
2061#[cfg(test)]
2062mod tests {
2063    use super::*;
2064    use breezyshim::controldir::{create_standalone_workingtree, ControlDirFormat};
2065    use breezyshim::tree::{MutableTree, WorkingTree};
2066    use std::path::Path;
2067
2068    pub const COMMITTER: &str = "Testsuite <lintian-brush@example.com>";
2069
2070    mod test_run_lintian_fixer {
2071        use super::*;
2072
2073        #[derive(Debug)]
2074        struct DummyFixer {
2075            name: String,
2076            lintian_tags: Vec<String>,
2077        }
2078
2079        impl DummyFixer {
2080            fn new(name: &str, lintian_tags: &[&str]) -> Self {
2081                Self {
2082                    name: name.to_string(),
2083                    lintian_tags: lintian_tags.iter().map(|t| t.to_string()).collect(),
2084                }
2085            }
2086        }
2087
2088        impl Fixer for DummyFixer {
2089            fn name(&self) -> String {
2090                self.name.clone()
2091            }
2092
2093            fn path(&self) -> std::path::PathBuf {
2094                std::path::PathBuf::from("/dev/null")
2095            }
2096
2097            fn lintian_tags(&self) -> Vec<String> {
2098                self.lintian_tags.clone()
2099            }
2100
2101            fn run(
2102                &self,
2103                basedir: &std::path::Path,
2104                package: &str,
2105                _current_version: &Version,
2106                _preferences: &FixerPreferences,
2107                _timeout: Option<chrono::Duration>,
2108            ) -> Result<FixerResult, FixerError> {
2109                std::fs::write(basedir.join("debian/control"), "a new line\n").unwrap();
2110                Ok(FixerResult {
2111                    description: "Fixed some tag.\nExtended description.".to_string(),
2112                    patch_name: None,
2113                    certainty: Some(Certainty::Certain),
2114                    fixed_lintian_issues: vec![LintianIssue {
2115                        tag: Some("some-tag".to_string()),
2116                        package: Some(package.to_string()),
2117                        info: None,
2118                        package_type: Some(PackageType::Source),
2119                    }],
2120                    overridden_lintian_issues: vec![],
2121                    revision_id: None,
2122                })
2123            }
2124        }
2125
2126        #[derive(Debug)]
2127        struct FailingFixer {
2128            name: String,
2129            lintian_tags: Vec<String>,
2130        }
2131
2132        impl FailingFixer {
2133            fn new(name: &str, lintian_tags: &[&str]) -> Self {
2134                Self {
2135                    name: name.to_string(),
2136                    lintian_tags: lintian_tags.iter().map(|t| t.to_string()).collect(),
2137                }
2138            }
2139        }
2140
2141        impl Fixer for FailingFixer {
2142            fn name(&self) -> String {
2143                self.name.clone()
2144            }
2145
2146            fn path(&self) -> std::path::PathBuf {
2147                std::path::PathBuf::from("/dev/null")
2148            }
2149
2150            fn lintian_tags(&self) -> Vec<String> {
2151                self.lintian_tags.clone()
2152            }
2153
2154            fn run(
2155                &self,
2156                basedir: &std::path::Path,
2157                _package: &str,
2158                _current_version: &Version,
2159                _preferences: &FixerPreferences,
2160                _timeout: Option<chrono::Duration>,
2161            ) -> Result<FixerResult, FixerError> {
2162                std::fs::write(basedir.join("debian/foo"), "blah").unwrap();
2163                std::fs::write(basedir.join("debian/control"), "foo\n").unwrap();
2164                Err(FixerError::ScriptFailed {
2165                    stderr: "Not successful".to_string(),
2166                    path: std::path::PathBuf::from("/dev/null"),
2167                    exit_code: 1,
2168                })
2169            }
2170        }
2171
2172        fn setup(version: Option<&str>) -> (tempfile::TempDir, WorkingTree) {
2173            let version = version.unwrap_or("0.1");
2174            let td = tempfile::tempdir().unwrap();
2175            let tree =
2176                create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
2177            tree.mkdir(std::path::Path::new("debian")).unwrap();
2178            std::fs::write(
2179                td.path().join("debian/control"),
2180                r#""Source: blah
2181Vcs-Git: https://example.com/blah
2182Testsuite: autopkgtest
2183
2184Binary: blah
2185Arch: all
2186
2187"#,
2188            )
2189            .unwrap();
2190            tree.add(&[std::path::Path::new("debian/control")]).unwrap();
2191            std::fs::write(
2192                td.path().join("debian/changelog"),
2193                format!(
2194                    r#"blah ({}) UNRELEASED; urgency=medium
2195
2196  * Initial release. (Closes: #911016)
2197
2198 -- Blah <example@debian.org>  Sat, 13 Oct 2018 11:21:39 +0100
2199"#,
2200                    version
2201                ),
2202            )
2203            .unwrap();
2204            tree.add(&[std::path::Path::new("debian/changelog")])
2205                .unwrap();
2206            tree.build_commit()
2207                .message("Initial thingy.")
2208                .committer(COMMITTER)
2209                .commit()
2210                .unwrap();
2211            (td, tree)
2212        }
2213
2214        #[test]
2215        fn test_fails() {
2216            let (td, tree) = setup(None);
2217            let lock = tree.lock_write().unwrap();
2218            let result = run_lintian_fixers(
2219                &tree,
2220                &[Box::new(FailingFixer::new("fail", &["some-tag"]))],
2221                Some(|| false),
2222                false,
2223                None,
2224                &FixerPreferences::default(),
2225                None,
2226                None,
2227                None,
2228                None,
2229            )
2230            .unwrap();
2231            std::mem::drop(lock);
2232            assert_eq!(0, result.success.len());
2233            assert_eq!(1, result.failed_fixers.len());
2234            let fixer = result.failed_fixers.get("fail").unwrap();
2235            assert!(fixer.contains("Not successful"));
2236
2237            let lock = tree.lock_read().unwrap();
2238            assert_eq!(
2239                Vec::<breezyshim::tree::TreeChange>::new(),
2240                tree.iter_changes(&tree.basis_tree().unwrap(), None, None, None)
2241                    .unwrap()
2242                    .collect::<Result<Vec<_>, _>>()
2243                    .unwrap()
2244            );
2245            std::mem::drop(lock);
2246            std::mem::drop(td);
2247        }
2248
2249        #[test]
2250        fn test_not_debian_tree() {
2251            let (td, tree) = setup(None);
2252            tree.remove(&[(std::path::Path::new("debian/changelog"))])
2253                .unwrap();
2254            std::fs::remove_file(td.path().join("debian/changelog")).unwrap();
2255            tree.build_commit()
2256                .message("not a debian dir")
2257                .committer(COMMITTER)
2258                .commit()
2259                .unwrap();
2260            let lock = tree.lock_write().unwrap();
2261
2262            assert!(matches!(
2263                run_lintian_fixers(
2264                    &tree,
2265                    &[Box::new(DummyFixer::new("dummy", &["some-tag"][..]))],
2266                    Some(|| false),
2267                    false,
2268                    None,
2269                    &FixerPreferences::default(),
2270                    None,
2271                    None,
2272                    None,
2273                    None,
2274                ),
2275                Err(OverallError::NotDebianPackage(_))
2276            ));
2277            std::mem::drop(lock);
2278            std::mem::drop(td);
2279        }
2280
2281        #[test]
2282        fn test_simple_modify() {
2283            let (td, tree) = setup(None);
2284            let lock = tree.lock_write().unwrap();
2285            let result = run_lintian_fixers(
2286                &tree,
2287                &[Box::new(DummyFixer::new("dummy", &["some-tag"]))],
2288                Some(|| false),
2289                false,
2290                Some(COMMITTER),
2291                &FixerPreferences::default(),
2292                None,
2293                None,
2294                None,
2295                None,
2296            )
2297            .unwrap();
2298            let revid = tree.last_revision().unwrap();
2299            std::mem::drop(lock);
2300
2301            assert_eq!(
2302                vec![(
2303                    FixerResult::new(
2304                        "Fixed some tag.\nExtended description.".to_string(),
2305                        None,
2306                        Some(Certainty::Certain),
2307                        None,
2308                        Some(revid),
2309                        vec![LintianIssue {
2310                            tag: Some("some-tag".to_string()),
2311                            package: Some("blah".to_string()),
2312                            info: None,
2313                            package_type: Some(PackageType::Source),
2314                        }],
2315                        None,
2316                    ),
2317                    "Fixed some tag.".to_string()
2318                )],
2319                result.success,
2320            );
2321            assert_eq!(maplit::hashmap! {}, result.failed_fixers);
2322            assert_eq!(2, tree.branch().revno());
2323            let lines = tree
2324                .get_file_lines(std::path::Path::new("debian/control"))
2325                .unwrap();
2326            assert_eq!(lines.last().unwrap(), &b"a new line\n".to_vec());
2327            std::mem::drop(td);
2328        }
2329
2330        #[test]
2331        fn test_simple_modify_too_uncertain() {
2332            let (td, tree) = setup(None);
2333
2334            #[derive(Debug)]
2335            struct UncertainFixer {
2336                name: String,
2337                lintian_tags: Vec<String>,
2338            }
2339
2340            impl UncertainFixer {
2341                fn new(name: &str, lintian_tags: &[&str]) -> Self {
2342                    Self {
2343                        name: name.to_string(),
2344                        lintian_tags: lintian_tags.iter().map(|t| t.to_string()).collect(),
2345                    }
2346                }
2347            }
2348
2349            impl Fixer for UncertainFixer {
2350                fn name(&self) -> String {
2351                    self.name.clone()
2352                }
2353
2354                fn path(&self) -> std::path::PathBuf {
2355                    std::path::PathBuf::from("/dev/null")
2356                }
2357
2358                fn lintian_tags(&self) -> Vec<String> {
2359                    self.lintian_tags.clone()
2360                }
2361
2362                fn run(
2363                    &self,
2364                    basedir: &std::path::Path,
2365                    _package: &str,
2366                    _current_version: &Version,
2367                    _preferences: &FixerPreferences,
2368                    _timeout: Option<chrono::Duration>,
2369                ) -> Result<FixerResult, FixerError> {
2370                    std::fs::write(basedir.join("debian/somefile"), "test").unwrap();
2371                    Ok(FixerResult {
2372                        description: "Renamed a file.".to_string(),
2373                        patch_name: None,
2374                        certainty: Some(Certainty::Possible),
2375                        fixed_lintian_issues: vec![],
2376                        overridden_lintian_issues: vec![],
2377                        revision_id: None,
2378                    })
2379                }
2380            }
2381
2382            let lock_write = tree.lock_write().unwrap();
2383
2384            let result = run_lintian_fixer(
2385                &tree,
2386                &UncertainFixer::new("dummy", &["some-tag"]),
2387                Some(COMMITTER),
2388                || false,
2389                &FixerPreferences {
2390                    minimum_certainty: Some(Certainty::Certain),
2391                    ..Default::default()
2392                },
2393                &mut None,
2394                Path::new(""),
2395                None,
2396                None,
2397                None,
2398                None,
2399            );
2400
2401            assert!(
2402                matches!(result, Err(FixerError::NotCertainEnough(..))),
2403                "{:?}",
2404                result
2405            );
2406            assert_eq!(1, tree.branch().revno());
2407            std::mem::drop(lock_write);
2408            std::mem::drop(td);
2409        }
2410
2411        #[test]
2412        fn test_simple_modify_acceptably_uncertain() {
2413            let (td, tree) = setup(None);
2414
2415            #[derive(Debug)]
2416            struct UncertainFixer {
2417                name: String,
2418                lintian_tags: Vec<String>,
2419            }
2420
2421            impl UncertainFixer {
2422                fn new(name: &str, lintian_tags: &[&str]) -> Self {
2423                    Self {
2424                        name: name.to_string(),
2425                        lintian_tags: lintian_tags.iter().map(|t| t.to_string()).collect(),
2426                    }
2427                }
2428            }
2429
2430            impl Fixer for UncertainFixer {
2431                fn name(&self) -> String {
2432                    self.name.clone()
2433                }
2434
2435                fn path(&self) -> std::path::PathBuf {
2436                    std::path::PathBuf::from("/dev/null")
2437                }
2438
2439                fn lintian_tags(&self) -> Vec<String> {
2440                    self.lintian_tags.clone()
2441                }
2442
2443                fn run(
2444                    &self,
2445                    basedir: &std::path::Path,
2446                    _package: &str,
2447                    _current_version: &Version,
2448                    _preferences: &FixerPreferences,
2449                    _timeout: Option<chrono::Duration>,
2450                ) -> Result<FixerResult, FixerError> {
2451                    std::fs::write(basedir.join("debian/somefile"), "test").unwrap();
2452                    Ok(FixerResult {
2453                        description: "Renamed a file.".to_string(),
2454                        patch_name: None,
2455                        certainty: Some(Certainty::Possible),
2456                        fixed_lintian_issues: vec![],
2457                        overridden_lintian_issues: vec![],
2458                        revision_id: None,
2459                    })
2460                }
2461            }
2462
2463            let lock_write = tree.lock_write().unwrap();
2464
2465            let (_result, summary) = run_lintian_fixer(
2466                &tree,
2467                &UncertainFixer::new("dummy", &["some-tag"]),
2468                Some("Testsuite <lintian-brush@example.com>"),
2469                || false,
2470                &FixerPreferences {
2471                    minimum_certainty: Some(Certainty::Possible),
2472                    ..Default::default()
2473                },
2474                &mut None,
2475                Path::new(""),
2476                None,
2477                None,
2478                None,
2479                None,
2480            )
2481            .unwrap();
2482
2483            assert_eq!("Renamed a file.", summary);
2484
2485            assert_eq!(2, tree.branch().revno());
2486
2487            std::mem::drop(lock_write);
2488            std::mem::drop(td);
2489        }
2490
2491        #[test]
2492        fn test_new_file() {
2493            let (td, tree) = setup(None);
2494
2495            #[derive(Debug)]
2496            struct NewFileFixer {
2497                name: String,
2498                lintian_tags: Vec<String>,
2499            }
2500
2501            impl NewFileFixer {
2502                fn new(name: &str, lintian_tags: &[&str]) -> Self {
2503                    Self {
2504                        name: name.to_string(),
2505                        lintian_tags: lintian_tags.iter().map(|t| t.to_string()).collect(),
2506                    }
2507                }
2508            }
2509
2510            impl Fixer for NewFileFixer {
2511                fn name(&self) -> String {
2512                    self.name.clone()
2513                }
2514
2515                fn path(&self) -> std::path::PathBuf {
2516                    std::path::PathBuf::from("/dev/null")
2517                }
2518
2519                fn lintian_tags(&self) -> Vec<String> {
2520                    self.lintian_tags.clone()
2521                }
2522
2523                fn run(
2524                    &self,
2525                    basedir: &std::path::Path,
2526                    package: &str,
2527                    _current_version: &Version,
2528                    _preferences: &FixerPreferences,
2529                    _timeout: Option<chrono::Duration>,
2530                ) -> Result<FixerResult, FixerError> {
2531                    std::fs::write(basedir.join("debian/somefile"), "test").unwrap();
2532                    Ok(FixerResult {
2533                        description: "Created new file.".to_string(),
2534                        patch_name: None,
2535                        certainty: None,
2536                        fixed_lintian_issues: vec![LintianIssue {
2537                            tag: Some("some-tag".to_string()),
2538                            package: Some(package.to_string()),
2539                            info: None,
2540                            package_type: Some(PackageType::Source),
2541                        }],
2542                        overridden_lintian_issues: vec![],
2543                        revision_id: None,
2544                    })
2545                }
2546            }
2547
2548            let lock_write = tree.lock_write().unwrap();
2549
2550            let (result, summary) = run_lintian_fixer(
2551                &tree,
2552                &NewFileFixer::new("new-file", &["some-tag"]),
2553                Some(COMMITTER),
2554                || false,
2555                &FixerPreferences::default(),
2556                &mut None,
2557                Path::new(""),
2558                None,
2559                None,
2560                None,
2561                None,
2562            )
2563            .unwrap();
2564
2565            assert_eq!("Created new file.", summary);
2566            assert_eq!(result.certainty, None);
2567            assert_eq!(result.fixed_lintian_tags(), &["some-tag"]);
2568            let rev = tree
2569                .branch()
2570                .repository()
2571                .get_revision(&tree.last_revision().unwrap())
2572                .unwrap();
2573            assert_eq!(
2574                rev.message,
2575                "Created new file.\n\nChanges-By: lintian-brush\nFixes: lintian: some-tag\nSee-also: https://lintian.debian.org/tags/some-tag.html\n"
2576            );
2577            assert_eq!(2, tree.branch().revno());
2578            let basis_tree = tree.branch().basis_tree().unwrap();
2579            let basis_lock = basis_tree.lock_read().unwrap();
2580            assert_eq!(
2581                basis_tree
2582                    .get_file_text(Path::new("debian/somefile"))
2583                    .unwrap(),
2584                b"test"
2585            );
2586            std::mem::drop(basis_lock);
2587            std::mem::drop(lock_write);
2588            std::mem::drop(td);
2589        }
2590
2591        #[test]
2592        fn test_rename_file() {
2593            let (td, tree) = setup(None);
2594
2595            #[derive(Debug)]
2596            struct RenameFileFixer {
2597                name: String,
2598                lintian_tags: Vec<String>,
2599            }
2600
2601            impl RenameFileFixer {
2602                fn new(name: &str, lintian_tags: &[&str]) -> Self {
2603                    Self {
2604                        name: name.to_string(),
2605                        lintian_tags: lintian_tags.iter().map(|t| t.to_string()).collect(),
2606                    }
2607                }
2608            }
2609
2610            impl Fixer for RenameFileFixer {
2611                fn name(&self) -> String {
2612                    self.name.clone()
2613                }
2614
2615                fn path(&self) -> std::path::PathBuf {
2616                    std::path::PathBuf::from("/dev/null")
2617                }
2618
2619                fn lintian_tags(&self) -> Vec<String> {
2620                    self.lintian_tags.clone()
2621                }
2622
2623                fn run(
2624                    &self,
2625                    basedir: &std::path::Path,
2626                    _package: &str,
2627                    _current_version: &Version,
2628                    _preferences: &FixerPreferences,
2629                    _timeout: Option<chrono::Duration>,
2630                ) -> Result<FixerResult, FixerError> {
2631                    std::fs::rename(
2632                        basedir.join("debian/control"),
2633                        basedir.join("debian/control.blah"),
2634                    )
2635                    .unwrap();
2636                    Ok(FixerResult {
2637                        description: "Renamed a file.".to_string(),
2638                        patch_name: None,
2639                        certainty: None,
2640                        fixed_lintian_issues: vec![],
2641                        overridden_lintian_issues: vec![],
2642                        revision_id: None,
2643                    })
2644                }
2645            }
2646
2647            let orig_basis_tree = tree.branch().basis_tree().unwrap();
2648            let lock_write = tree.lock_write().unwrap();
2649            let (result, summary) = run_lintian_fixer(
2650                &tree,
2651                &RenameFileFixer::new("rename", &["some-tag"]),
2652                Some(COMMITTER),
2653                || false,
2654                &FixerPreferences::default(),
2655                &mut None,
2656                Path::new(""),
2657                None,
2658                None,
2659                None,
2660                None,
2661            )
2662            .unwrap();
2663            assert_eq!("Renamed a file.", summary);
2664            assert_eq!(result.certainty, None);
2665            assert_eq!(2, tree.branch().revno());
2666            let basis_tree = tree.branch().basis_tree().unwrap();
2667            let basis_lock = basis_tree.lock_read().unwrap();
2668            let orig_basis_tree_lock = orig_basis_tree.lock_read().unwrap();
2669            assert!(!basis_tree.has_filename(Path::new("debian/control")));
2670            assert!(basis_tree.has_filename(Path::new("debian/control.blah")));
2671            assert_ne!(
2672                orig_basis_tree.get_revision_id(),
2673                basis_tree.get_revision_id()
2674            );
2675            std::mem::drop(orig_basis_tree_lock);
2676            std::mem::drop(basis_lock);
2677            std::mem::drop(lock_write);
2678            std::mem::drop(td);
2679        }
2680
2681        #[test]
2682        fn test_empty_change() {
2683            let (td, tree) = setup(None);
2684
2685            #[derive(Debug)]
2686            struct EmptyFixer {
2687                name: String,
2688                lintian_tags: Vec<String>,
2689            }
2690
2691            impl EmptyFixer {
2692                fn new(name: &str, lintian_tags: &[&str]) -> Self {
2693                    Self {
2694                        name: name.to_string(),
2695                        lintian_tags: lintian_tags.iter().map(|t| t.to_string()).collect(),
2696                    }
2697                }
2698            }
2699
2700            impl Fixer for EmptyFixer {
2701                fn name(&self) -> String {
2702                    self.name.clone()
2703                }
2704
2705                fn path(&self) -> std::path::PathBuf {
2706                    std::path::PathBuf::from("/dev/null")
2707                }
2708
2709                fn lintian_tags(&self) -> Vec<String> {
2710                    self.lintian_tags.clone()
2711                }
2712
2713                fn run(
2714                    &self,
2715                    _basedir: &std::path::Path,
2716                    _package: &str,
2717                    _current_version: &Version,
2718                    _preferences: &FixerPreferences,
2719                    _timeout: Option<chrono::Duration>,
2720                ) -> Result<FixerResult, FixerError> {
2721                    Ok(FixerResult {
2722                        description: "I didn't actually change anything.".to_string(),
2723                        patch_name: None,
2724                        certainty: None,
2725                        fixed_lintian_issues: vec![],
2726                        overridden_lintian_issues: vec![],
2727                        revision_id: None,
2728                    })
2729                }
2730            }
2731
2732            let lock_write = tree.lock_write().unwrap();
2733
2734            let result = run_lintian_fixer(
2735                &tree,
2736                &EmptyFixer::new("empty", &["some-tag"]),
2737                Some(COMMITTER),
2738                || false,
2739                &FixerPreferences::default(),
2740                &mut None,
2741                Path::new(""),
2742                None,
2743                None,
2744                None,
2745                None,
2746            );
2747
2748            assert!(matches!(result, Err(FixerError::NoChanges)), "{:?}", result);
2749            assert_eq!(1, tree.branch().revno());
2750
2751            assert_eq!(
2752                Vec::<breezyshim::tree::TreeChange>::new(),
2753                tree.iter_changes(&tree.basis_tree().unwrap(), None, None, None)
2754                    .unwrap()
2755                    .collect::<Result<Vec<_>, _>>()
2756                    .unwrap()
2757            );
2758
2759            std::mem::drop(lock_write);
2760
2761            std::mem::drop(td);
2762        }
2763
2764        #[test]
2765        fn test_upstream_change() {
2766            let (td, tree) = setup(Some("0.1-1"));
2767
2768            #[derive(Debug)]
2769            struct NewFileFixer {
2770                name: String,
2771                lintian_tags: Vec<String>,
2772            }
2773
2774            impl NewFileFixer {
2775                fn new(name: &str, lintian_tags: &[&str]) -> Self {
2776                    Self {
2777                        name: name.to_string(),
2778                        lintian_tags: lintian_tags.iter().map(|t| t.to_string()).collect(),
2779                    }
2780                }
2781            }
2782
2783            impl Fixer for NewFileFixer {
2784                fn name(&self) -> String {
2785                    self.name.clone()
2786                }
2787
2788                fn path(&self) -> std::path::PathBuf {
2789                    std::path::PathBuf::from("/dev/null")
2790                }
2791
2792                fn lintian_tags(&self) -> Vec<String> {
2793                    self.lintian_tags.clone()
2794                }
2795
2796                fn run(
2797                    &self,
2798                    basedir: &std::path::Path,
2799                    _package: &str,
2800                    _current_version: &Version,
2801                    _preferences: &FixerPreferences,
2802                    _timeout: Option<chrono::Duration>,
2803                ) -> Result<FixerResult, FixerError> {
2804                    std::fs::write(basedir.join("configure.ac"), "AC_INIT(foo, bar)\n").unwrap();
2805                    Ok(FixerResult {
2806                        description: "Created new configure.ac.".to_string(),
2807                        patch_name: Some("add-config".to_string()),
2808                        certainty: None,
2809                        fixed_lintian_issues: vec![],
2810                        overridden_lintian_issues: vec![],
2811                        revision_id: None,
2812                    })
2813                }
2814            }
2815
2816            let lock = tree.lock_write().unwrap();
2817
2818            let (result, summary) = run_lintian_fixer(
2819                &tree,
2820                &NewFileFixer::new("add-config", &["add-config"]),
2821                Some(COMMITTER),
2822                || false,
2823                &FixerPreferences::default(),
2824                &mut None,
2825                Path::new(""),
2826                Some(
2827                    chrono::DateTime::parse_from_rfc3339("2020-09-08T00:36:35Z")
2828                        .unwrap()
2829                        .naive_utc(),
2830                ),
2831                None,
2832                None,
2833                None,
2834            )
2835            .unwrap();
2836            assert_eq!(
2837                summary,
2838                "Add patch add-config.patch: Created new configure.ac."
2839            );
2840            assert_eq!(result.certainty, None);
2841            let rev = tree
2842                .branch()
2843                .repository()
2844                .get_revision(&tree.last_revision().unwrap())
2845                .unwrap();
2846            assert_eq!(
2847                rev.message,
2848                "Created new configure.ac.\n\nChanges-By: lintian-brush\n"
2849            );
2850            assert_eq!(2, tree.branch().revno());
2851            let basis_tree = tree.branch().basis_tree().unwrap();
2852            let basis_lock = basis_tree.lock_read().unwrap();
2853            assert_eq!(
2854                basis_tree
2855                    .get_file_text(Path::new("debian/patches/series"))
2856                    .unwrap(),
2857                b"add-config.patch\n"
2858            );
2859            let lines = basis_tree
2860                .get_file_lines(Path::new("debian/patches/add-config.patch"))
2861                .unwrap();
2862            assert_eq!(lines[0], b"Description: Created new configure.ac.\n");
2863            assert_eq!(lines[1], b"Origin: other\n");
2864            assert_eq!(lines[2], b"Last-Update: 2020-09-08\n");
2865            assert_eq!(lines[3], b"---\n");
2866            assert_eq!(lines[4], b"=== added file 'configure.ac'\n");
2867            assert_eq!(
2868                &lines[5][..(b"--- a/configure.ac".len())],
2869                b"--- a/configure.ac"
2870            );
2871            assert_eq!(
2872                &lines[6][..(b"+++ b/configure.ac".len())],
2873                b"+++ b/configure.ac"
2874            );
2875            assert_eq!(lines[7], b"@@ -0,0 +1,1 @@\n");
2876            assert_eq!(lines[8], b"+AC_INIT(foo, bar)\n");
2877
2878            std::mem::drop(basis_lock);
2879            std::mem::drop(lock);
2880            std::mem::drop(td);
2881        }
2882
2883        #[test]
2884        fn test_upstream_change_stacked() {
2885            let (td, tree) = setup(Some("0.1-1"));
2886
2887            std::fs::create_dir(td.path().join("debian/patches")).unwrap();
2888            std::fs::write(td.path().join("debian/patches/series"), "foo\n").unwrap();
2889            std::fs::write(
2890                td.path().join("debian/patches/foo"),
2891                r###"--- /dev/null	2020-09-07 13:26:27.546468905 +0000
2892+++ a	2020-09-08 01:26:25.811742671 +0000
2893@@ -0,0 +1 @@
2894+foo
2895"###,
2896            )
2897            .unwrap();
2898            tree.add(&[
2899                Path::new("debian/patches"),
2900                Path::new("debian/patches/series"),
2901                Path::new("debian/patches/foo"),
2902            ])
2903            .unwrap();
2904            tree.build_commit()
2905                .committer(COMMITTER)
2906
2907                .message("Add patches").commit().unwrap();
2908
2909            #[derive(Debug)]
2910            struct NewFileFixer {
2911                name: String,
2912                lintian_tags: Vec<String>,
2913            }
2914
2915            impl NewFileFixer {
2916                fn new(name: &str, lintian_tags: &[&str]) -> Self {
2917                    Self {
2918                        name: name.to_string(),
2919                        lintian_tags: lintian_tags.iter().map(|t| t.to_string()).collect(),
2920                    }
2921                }
2922            }
2923
2924            impl Fixer for NewFileFixer {
2925                fn name(&self) -> String {
2926                    self.name.clone()
2927                }
2928
2929                fn path(&self) -> std::path::PathBuf {
2930                    std::path::PathBuf::from("/dev/null")
2931                }
2932
2933                fn lintian_tags(&self) -> Vec<String> {
2934                    self.lintian_tags.clone()
2935                }
2936
2937                fn run(
2938                    &self,
2939                    basedir: &std::path::Path,
2940                    _package: &str,
2941                    _current_version: &Version,
2942                    _preferences: &FixerPreferences,
2943                    _timeout: Option<chrono::Duration>,
2944                ) -> Result<FixerResult, FixerError> {
2945                    std::fs::write(basedir.join("configure.ac"), "AC_INIT(foo, bar)\n").unwrap();
2946                    Ok(FixerResult {
2947                        description: "Created new configure.ac.".to_string(),
2948                        patch_name: Some("add-config".to_string()),
2949                        certainty: None,
2950                        fixed_lintian_issues: vec![],
2951                        overridden_lintian_issues: vec![],
2952                        revision_id: None,
2953                    })
2954                }
2955            }
2956
2957            let lock = tree.lock_write().unwrap();
2958
2959            let result = run_lintian_fixer(
2960                &tree,
2961                &NewFileFixer::new("add-config", &["add-config"]),
2962                Some(COMMITTER),
2963                || false,
2964                &FixerPreferences::default(),
2965                &mut None,
2966                Path::new(""),
2967                Some(
2968                    chrono::DateTime::parse_from_rfc3339("2020-09-08T00:36:35Z")
2969                        .unwrap()
2970                        .naive_utc(),
2971                ),
2972                None,
2973                None,
2974                None,
2975            );
2976
2977            std::mem::drop(lock);
2978
2979            assert!(matches!(
2980                result,
2981                Err(FixerError::FailedPatchManipulation(..))
2982            ));
2983            std::mem::drop(td);
2984        }
2985
2986        fn make_package_tree(path: &Path, format: &str) -> WorkingTree {
2987            let tree = create_standalone_workingtree(path, format).unwrap();
2988            std::fs::create_dir(path.join("debian")).unwrap();
2989            std::fs::write(
2990                path.join("debian/control"),
2991                r#""Source: blah
2992Vcs-Git: https://example.com/blah
2993Testsuite: autopkgtest
2994
2995Binary: blah
2996Arch: all
2997
2998"#,
2999            )
3000            .unwrap();
3001            std::fs::write(
3002                path.join("debian/changelog"),
3003                r#"blah (0.1-1) UNRELEASED; urgency=medium
3004
3005  * Initial release. (Closes: #911016)
3006
3007 -- Blah <example@debian.org>  Sat, 13 Oct 2018 11:21:39 +0100
3008"#,
3009            )
3010            .unwrap();
3011            tree.add(&[
3012                Path::new("debian"),
3013                Path::new("debian/changelog"),
3014                Path::new("debian/control"),
3015            ])
3016            .unwrap();
3017            tree.build_commit()
3018                .committer(COMMITTER)
3019                .message("Initial thingy.")
3020                .commit()
3021                .unwrap();
3022            tree
3023        }
3024
3025        fn make_change(tree: &WorkingTree, committer: Option<&str>) {
3026            let lock = tree.lock_write().unwrap();
3027
3028            let (result, summary) = run_lintian_fixer(
3029                tree,
3030                &DummyFixer::new("dummy", &["some-tag"]),
3031                committer,
3032                || false,
3033                &FixerPreferences::default(),
3034                &mut None,
3035                Path::new(""),
3036                None,
3037                None,
3038                None,
3039                None,
3040            )
3041            .unwrap();
3042            assert_eq!(summary, "Fixed some tag.");
3043            assert_eq!(vec!["some-tag"], result.fixed_lintian_tags());
3044            assert_eq!(Some(Certainty::Certain), result.certainty);
3045            assert_eq!(2, tree.branch().revno());
3046            let lines = tree.get_file_lines(Path::new("debian/control")).unwrap();
3047            assert_eq!(lines.last().unwrap(), b"a new line\n");
3048            std::mem::drop(lock);
3049        }
3050
3051        #[test]
3052        fn test_honors_tree_committer_specified() {
3053            let td = tempfile::tempdir().unwrap();
3054            let tree = make_package_tree(td.path(), "git");
3055
3056            make_change(&tree, Some("Jane Example <jane@example.com>"));
3057
3058            let rev = tree
3059                .branch()
3060                .repository()
3061                .get_revision(&tree.branch().last_revision())
3062                .unwrap();
3063            assert_eq!(rev.committer, "Jane Example <jane@example.com>");
3064        }
3065
3066        #[test]
3067        fn test_honors_tree_committer_config() {
3068            let td = tempfile::tempdir().unwrap();
3069            let tree = make_package_tree(td.path(), "git");
3070            std::fs::write(
3071                td.path().join(".git/config"),
3072                r###"
3073[user]
3074  email = jane@example.com
3075  name = Jane Example
3076"###,
3077            )
3078            .unwrap();
3079
3080            make_change(&tree, None);
3081
3082            let rev = tree
3083                .branch()
3084                .repository()
3085                .get_revision(&tree.branch().last_revision())
3086                .unwrap();
3087            assert_eq!(rev.committer, "Jane Example <jane@example.com>");
3088        }
3089    }
3090
3091    #[test]
3092    fn test_find_shell_scripts() {
3093        let td = tempfile::tempdir().unwrap();
3094
3095        let fixers = td.path().join("fixers");
3096        std::fs::create_dir(&fixers).unwrap();
3097
3098        std::fs::create_dir(fixers.join("anotherdir")).unwrap();
3099        std::fs::write(fixers.join("foo.sh"), "echo 'hello'").unwrap();
3100        std::fs::write(fixers.join("bar.sh"), "echo 'hello'").unwrap();
3101        std::fs::write(fixers.join("i-fix-aanother-tag.py"), "print('hello')").unwrap();
3102        std::fs::write(fixers.join(".hidden"), "echo 'hello'").unwrap();
3103        std::fs::write(fixers.join("backup-file.sh~"), "echo 'hello'").unwrap();
3104        std::fs::write(fixers.join("no-extension"), "echo 'hello'").unwrap();
3105        std::fs::write(
3106            fixers.join("index.desc"),
3107            r###"
3108
3109- script: foo.sh
3110  lintian-tags:
3111   - i-fix-a-tag
3112
3113- script: bar.sh
3114  lintian-tags:
3115   - i-fix-another-tag
3116   - no-extension
3117"###,
3118        )
3119        .unwrap();
3120
3121        let fixers = available_lintian_fixers(Some(&fixers), Some(false))
3122            .unwrap()
3123            .collect::<Vec<_>>();
3124        assert_eq!(2, fixers.len());
3125        assert_eq!(fixers[0].name(), "foo");
3126        assert_eq!(fixers[1].name(), "bar");
3127    }
3128
3129    mod many_result_tests {
3130        use super::*;
3131
3132        #[test]
3133        fn test_empty() {
3134            let result = ManyResult::default();
3135            assert_eq!(Certainty::Certain, result.minimum_success_certainty());
3136        }
3137
3138        #[test]
3139        fn test_no_certainty() {
3140            let mut result = ManyResult::default();
3141            result.success.push((
3142                FixerResult::new(
3143                    "Do bla".to_string(),
3144                    Some(vec!["tag-a".to_string()]),
3145                    None,
3146                    None,
3147                    None,
3148                    vec![],
3149                    None,
3150                ),
3151                "summary".to_string(),
3152            ));
3153            assert_eq!(Certainty::Certain, result.minimum_success_certainty());
3154        }
3155
3156        #[test]
3157        fn test_possible() {
3158            let mut result = ManyResult::default();
3159            result.success.push((
3160                FixerResult::new(
3161                    "Do bla".to_string(),
3162                    Some(vec!["tag-a".to_string()]),
3163                    Some(Certainty::Possible),
3164                    None,
3165                    None,
3166                    vec![],
3167                    None,
3168                ),
3169                "summary".to_string(),
3170            ));
3171            result.success.push((
3172                FixerResult::new(
3173                    "Do bloeh".to_string(),
3174                    Some(vec!["tag-b".to_string()]),
3175                    Some(Certainty::Certain),
3176                    None,
3177                    None,
3178                    vec![],
3179                    None,
3180                ),
3181                "summary".to_string(),
3182            ));
3183            assert_eq!(Certainty::Possible, result.minimum_success_certainty());
3184        }
3185    }
3186}
3187
3188#[cfg(test)]
3189mod fixer_tests;