use crate::declare_detector;
use crate::diagnostic::{Action, Deb822Action, Diagnostic, ParagraphSelector};
use crate::{Certainty, FixerError, FixerPreferences, LintianIssue, Visibility};
use debian_workspace::Workspace;
use regex::Regex;
use std::path::{Path, PathBuf};
fn dh_make_vcs_regex() -> Regex {
Regex::new(
r"^#\s*Vcs-(?:Git|Browser):\s*(?:git|http)://git\.debian\.org/(?:\?p=)?collab-maint/<pkg>\.git",
)
.unwrap()
}
fn scan_paragraph(
paragraph: &deb822_lossless::Paragraph,
selector: &ParagraphSelector,
re: &Regex,
control_rel: &Path,
actions: &mut Vec<Action>,
first_match: &mut Option<String>,
) {
for entry in paragraph.entries() {
let Some(field) = entry.key() else {
continue;
};
let with_comments = entry.value_with_comments();
let comment_lines: Vec<&str> = with_comments
.lines()
.filter(|line| line.starts_with('#'))
.collect();
let matching: Vec<&str> = comment_lines
.iter()
.copied()
.filter(|line| re.is_match(line))
.collect();
if matching.is_empty() {
continue;
}
if matching.len() != comment_lines.len() {
continue;
}
if first_match.is_none() {
*first_match = Some(matching[0].to_string());
}
actions.push(Action::Deb822(Deb822Action::DropFieldComments {
file: control_rel.to_path_buf(),
paragraph: selector.clone(),
field,
}));
}
}
pub fn detect(
ws: &dyn Workspace,
_preferences: &FixerPreferences,
) -> Result<Vec<Diagnostic>, FixerError> {
let control = match ws.parsed_control() {
Ok(c) => c,
Err(debian_workspace::Error::NotFound) => return Ok(Vec::new()),
Err(e) => return Err(e.into()),
};
let control_rel = PathBuf::from("debian/control");
let re = dh_make_vcs_regex();
let mut actions = Vec::new();
let mut first_match = None;
if let Some(source) = control.source() {
scan_paragraph(
source.as_deb822(),
&ParagraphSelector::Source,
&re,
&control_rel,
&mut actions,
&mut first_match,
);
}
for binary in control.binaries() {
let Some(package) = binary.as_deb822().get("Package") else {
continue;
};
scan_paragraph(
binary.as_deb822(),
&ParagraphSelector::Binary { package },
&re,
&control_rel,
&mut actions,
&mut first_match,
);
}
if actions.is_empty() {
return Ok(Vec::new());
}
let issue = LintianIssue::source_with_info(
"control-file-contains-dh-make-vcs-comment",
Visibility::Warning,
vec![first_match.expect("a non-empty action set implies a matched comment")],
);
Ok(vec![Diagnostic::with_actions(
issue,
"debian/control contains commented-out dh_make Vcs lines.",
"Remove commented-out dh_make Vcs lines from debian/control.",
actions,
)
.with_certainty(Certainty::Certain)])
}
declare_detector! {
name: "control-file-contains-dh_make-vcs-comment",
tags: ["control-file-contains-dh-make-vcs-comment"],
triggers: [
debian_workspace::Trigger::File("debian/control"),
],
detect: |ws, prefs| detect(ws, prefs),
}
#[cfg(test)]
mod tests {
use super::*;
use crate::detector::Detector;
use crate::{FixerPreferences, Version};
use std::fs;
use tempfile::TempDir;
fn run_apply(base: &Path) -> Result<crate::FixerResult, FixerError> {
let version: Version = "1.0".parse().unwrap();
let adapter = DetectorImpl;
let ws = debian_workspace::fs_workspace::FsWorkspace::new(
base,
Some("test".into()),
Some(version),
);
adapter.apply(&ws, &FixerPreferences::default())
}
fn write_control(base: &Path, content: &str) {
let debian = base.join("debian");
fs::create_dir_all(&debian).unwrap();
fs::write(debian.join("control"), content).unwrap();
}
#[test]
fn test_removes_dh_make_vcs_comments() {
let tmp = TempDir::new().unwrap();
write_control(
tmp.path(),
"Source: test\n\
Build-Depends: debhelper-compat (= 13)\n\
Homepage: https://example.com/\n\
#Vcs-Git: git://git.debian.org/collab-maint/<pkg>.git\n\
#Vcs-Browser: http://git.debian.org/?p=collab-maint/<pkg>.git\n\
\n\
Package: test\n\
Architecture: any\n\
Depends: ${shlibs:Depends}, ${misc:Depends}\n",
);
let result = run_apply(tmp.path()).unwrap();
assert_eq!(result.fixed_lintian_issues.len(), 1);
assert_eq!(
result.fixed_lintian_issues[0].tag.as_deref(),
Some("control-file-contains-dh-make-vcs-comment")
);
assert_eq!(
fs::read_to_string(tmp.path().join("debian/control")).unwrap(),
"Source: test\n\
Build-Depends: debhelper-compat (= 13)\n\
Homepage: https://example.com/\n\
\n\
Package: test\n\
Architecture: any\n\
Depends: ${shlibs:Depends}, ${misc:Depends}\n",
);
}
#[test]
fn test_no_change_without_comments() {
let tmp = TempDir::new().unwrap();
write_control(
tmp.path(),
"Source: test\nHomepage: https://example.com/\n\nPackage: test\nArchitecture: all\n",
);
assert!(matches!(run_apply(tmp.path()), Err(FixerError::NoChanges)));
}
#[test]
fn test_ignores_unrelated_commented_relations() {
let tmp = TempDir::new().unwrap();
let original = "Source: test\n\
Build-Depends:\n\
\u{20}debhelper-compat (= 13),\n\
#python3-nose,\n\
\u{20}python3\n\
\n\
Package: test\n\
Architecture: all\n";
write_control(tmp.path(), original);
assert!(matches!(run_apply(tmp.path()), Err(FixerError::NoChanges)));
}
#[test]
fn test_keeps_foreign_comment_in_same_field() {
let tmp = TempDir::new().unwrap();
let original = "Source: test\n\
Homepage: https://example.com/\n\
# upstream moved hosts\n\
#Vcs-Git: git://git.debian.org/collab-maint/<pkg>.git\n\
\n\
Package: test\n\
Architecture: all\n";
write_control(tmp.path(), original);
assert!(matches!(run_apply(tmp.path()), Err(FixerError::NoChanges)));
}
#[test]
fn test_no_control_file() {
let tmp = TempDir::new().unwrap();
assert!(matches!(run_apply(tmp.path()), Err(FixerError::NoChanges)));
}
}