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
385pub trait Fixer: std::fmt::Debug + Sync {
389 fn name(&self) -> String;
391
392 fn path(&self) -> std::path::PathBuf;
394
395 fn lintian_tags(&self) -> Vec<String>;
397
398 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#[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
1070pub 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 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
1115pub 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
1342pub 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 ¤t_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 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 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 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
1622pub 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 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 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 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 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 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 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;