use debversion::Version;
use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::{BufReader, Read};
use std::process::Command;
use std::str::FromStr;
use indicatif::ProgressBar;
use breezyshim::dirty_tracker::DirtyTreeTracker;
use breezyshim::error::Error;
use breezyshim::tree::{Tree, TreeChange, WorkingTree};
use breezyshim::workspace::{check_clean_tree, reset_tree_with_dirty_tracker};
use breezyshim::RevisionId;
use debian_analyzer::detect_gbp_dch::{guess_update_changelog, ChangelogBehaviour};
use debian_analyzer::{
add_changelog_entry, apply_or_revert, certainty_sufficient, get_committer, min_certainty,
ApplyError, Certainty, ChangelogError,
};
use debian_changelog::ChangeLog;
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum PackageType {
#[serde(rename = "source")]
Source,
#[serde(rename = "binary")]
Binary,
}
impl FromStr for PackageType {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"source" => Ok(PackageType::Source),
"binary" => Ok(PackageType::Binary),
_ => Err(format!("Invalid package type: {}", value)),
}
}
}
impl std::fmt::Display for PackageType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
PackageType::Source => write!(f, "source"),
PackageType::Binary => write!(f, "binary"),
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct LintianIssue {
pub package: Option<String>,
pub package_type: Option<PackageType>,
pub tag: Option<String>,
pub info: Option<Vec<String>>,
}
impl LintianIssue {
pub fn json(&self) -> serde_json::Value {
serde_json::json!({
"package": self.package,
"package_type": self.package_type.as_ref().map(|t| t.to_string()),
"tag": self.tag,
"info": self.info,
})
}
pub fn from_json(value: serde_json::Value) -> serde_json::Result<Self> {
serde_json::from_value(value)
}
pub fn just_tag(tag: String) -> Self {
Self {
package: None,
package_type: None,
tag: Some(tag),
info: None,
}
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum LintianIssueParseError {
InvalidPackageType(String),
}
#[cfg(feature = "python")]
impl pyo3::FromPyObject<'_> for LintianIssue {
fn extract_bound(ob: &pyo3::Bound<pyo3::PyAny>) -> pyo3::PyResult<Self> {
use pyo3::prelude::*;
let package = ob.getattr("package")?.extract::<Option<String>>()?;
let package_type = ob
.getattr("package_type")?
.extract::<Option<String>>()?
.map(|s| {
s.parse::<PackageType>()
.map_err(|e| pyo3::exceptions::PyValueError::new_err((e,)))
})
.transpose()?;
let tag = ob.getattr("tag")?.extract::<Option<String>>()?;
let info = ob.getattr("info")?.extract::<Option<Vec<String>>>()?;
Ok(Self {
package,
package_type,
tag,
info,
})
}
}
impl std::fmt::Display for LintianIssueParseError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
LintianIssueParseError::InvalidPackageType(s) => {
write!(f, "Invalid package type: {}", s)
}
}
}
}
impl std::error::Error for LintianIssueParseError {}
impl TryFrom<&str> for LintianIssue {
type Error = LintianIssueParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let value = value.trim();
let package_type;
let package;
let after = if let Some((before, after)) = value.split_once(':') {
if let Some((package_type_str, package_str)) = before.trim().split_once(' ') {
package_type = Some(match package_type_str {
"source" => PackageType::Source,
"binary" => PackageType::Binary,
_ => {
return Err(LintianIssueParseError::InvalidPackageType(
package_type_str.to_string(),
))
}
});
package = Some(package_str.to_string());
} else {
package_type = None;
package = Some(before.to_string());
}
after
} else {
package_type = None;
package = None;
value
};
let mut parts = after.trim().split(' ');
let tag = parts.next().map(|s| s.to_string());
let info: Vec<_> = parts.map(|s| s.to_string()).collect();
let info = if info.is_empty() { None } else { Some(info) };
Ok(Self {
package,
package_type,
tag,
info,
})
}
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct FixerResult {
pub description: String,
pub certainty: Option<Certainty>,
pub patch_name: Option<String>,
pub revision_id: Option<RevisionId>,
pub fixed_lintian_issues: Vec<LintianIssue>,
pub overridden_lintian_issues: Vec<LintianIssue>,
}
impl FixerResult {
pub fn new(
description: String,
fixed_lintian_tags: Option<Vec<String>>,
certainty: Option<Certainty>,
patch_name: Option<String>,
revision_id: Option<RevisionId>,
mut fixed_lintian_issues: Vec<LintianIssue>,
overridden_lintian_issues: Option<Vec<LintianIssue>>,
) -> Self {
if let Some(fixed_lintian_tags) = fixed_lintian_tags.as_ref() {
fixed_lintian_issues.extend(
fixed_lintian_tags
.iter()
.map(|tag| LintianIssue::just_tag(tag.to_string())),
);
}
Self {
description,
certainty,
patch_name,
revision_id,
fixed_lintian_issues,
overridden_lintian_issues: overridden_lintian_issues.unwrap_or_default(),
}
}
pub fn fixed_lintian_tags(&self) -> Vec<&str> {
self.fixed_lintian_issues
.iter()
.filter_map(|issue| issue.tag.as_deref())
.collect()
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum OutputParseError {
UnsupportedCertainty(String),
LintianIssueParseError(LintianIssueParseError),
}
impl std::fmt::Display for OutputParseError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
OutputParseError::UnsupportedCertainty(s) => {
write!(f, "Unsupported certainty: {}", s)
}
OutputParseError::LintianIssueParseError(e) => write!(f, "{}", e),
}
}
}
impl std::error::Error for OutputParseError {}
impl From<LintianIssueParseError> for OutputParseError {
fn from(value: LintianIssueParseError) -> Self {
Self::LintianIssueParseError(value)
}
}
pub fn parse_script_fixer_output(text: &str) -> Result<FixerResult, OutputParseError> {
let mut description: Vec<String> = Vec::new();
let mut overridden_issues: Vec<LintianIssue> = Vec::new();
let mut fixed_lintian_issues: Vec<LintianIssue> = Vec::new();
let mut fixed_lintian_tags: Vec<String> = Vec::new();
let mut certainty: Option<String> = None;
let mut patch_name: Option<String> = None;
let lines: Vec<&str> = text.split_terminator('\n').collect();
let mut i = 0;
while i < lines.len() {
if let Some((key, value)) = lines[i].split_once(':') {
match key.trim() {
"Fixed-Lintian-Tags" => {
fixed_lintian_tags.extend(value.split(',').map(|tag| tag.trim().to_owned()));
}
"Fixed-Lintian-Issues" => {
i += 1;
while i < lines.len() && lines[i].starts_with(' ') {
fixed_lintian_issues.push(LintianIssue::try_from(&lines[i][1..])?);
i += 1;
}
continue;
}
"Overridden-Lintian-Issues" => {
i += 1;
while i < lines.len() && lines[i].starts_with(' ') {
overridden_issues.push(LintianIssue::try_from(&lines[i][1..])?);
i += 1;
}
continue;
}
"Certainty" => {
certainty = Some(value.trim().to_owned());
}
"Patch-Name" => {
patch_name = Some(value.trim().to_owned());
}
_ => {
description.push(lines[i].to_owned());
}
}
} else {
description.push(lines[i].to_owned());
}
i += 1;
}
let certainty = certainty
.map(|c| c.parse())
.transpose()
.map_err(OutputParseError::UnsupportedCertainty)?;
let fixed_lintian_tags = if fixed_lintian_tags.is_empty() {
None
} else {
Some(fixed_lintian_tags)
};
let overridden_issues = if overridden_issues.is_empty() {
None
} else {
Some(overridden_issues)
};
Ok(FixerResult::new(
description.join("\n"),
fixed_lintian_tags,
certainty,
patch_name,
None,
fixed_lintian_issues,
overridden_issues,
))
}
pub fn determine_env(
package: &str,
current_version: &Version,
preferences: &FixerPreferences,
) -> std::collections::HashMap<String, String> {
let mut env = std::env::vars().collect::<std::collections::HashMap<_, _>>();
env.insert("DEB_SOURCE".to_owned(), package.to_owned());
env.insert("CURRENT_VERSION".to_owned(), current_version.to_string());
env.insert(
"COMPAT_RELEASE".to_owned(),
preferences
.compat_release
.as_deref()
.unwrap_or("sid")
.to_owned(),
);
env.insert(
"MINIMUM_CERTAINTY".to_owned(),
preferences
.minimum_certainty
.unwrap_or_default()
.to_string(),
);
env.insert(
"TRUST_PACKAGE".to_owned(),
preferences.trust_package.unwrap_or(false).to_string(),
);
env.insert(
"REFORMATTING".to_owned(),
if preferences.allow_reformatting.unwrap_or(false) {
"allow"
} else {
"disallow"
}
.to_owned(),
);
env.insert(
"NET_ACCESS".to_owned(),
if preferences.net_access.unwrap_or(true) {
"allow"
} else {
"disallow"
}
.to_owned(),
);
env.insert(
"OPINIONATED".to_owned(),
if preferences.opinionated.unwrap_or(false) {
"yes"
} else {
"no"
}
.to_owned(),
);
env.insert(
"DILIGENCE".to_owned(),
preferences.diligence.unwrap_or(0).to_string(),
);
env
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct FixerPreferences {
pub compat_release: Option<String>,
pub minimum_certainty: Option<Certainty>,
pub trust_package: Option<bool>,
pub allow_reformatting: Option<bool>,
pub net_access: Option<bool>,
pub opinionated: Option<bool>,
pub diligence: Option<i32>,
}
pub trait Fixer: std::fmt::Debug + Sync {
fn name(&self) -> String;
fn path(&self) -> std::path::PathBuf;
fn lintian_tags(&self) -> Vec<String>;
fn run(
&self,
basedir: &std::path::Path,
package: &str,
current_version: &Version,
preferences: &FixerPreferences,
timeout: Option<chrono::Duration>,
) -> Result<FixerResult, FixerError>;
}
#[cfg(feature = "python")]
#[derive(Debug)]
pub struct PythonScriptFixer {
path: std::path::PathBuf,
name: String,
lintian_tags: Vec<String>,
}
#[cfg(feature = "python")]
impl PythonScriptFixer {
pub fn new(name: String, lintian_tags: Vec<String>, path: std::path::PathBuf) -> Self {
Self {
path,
name,
lintian_tags,
}
}
}
#[cfg(feature = "python")]
fn run_inline_python_fixer(
path: &std::path::Path,
name: &str,
code: &str,
basedir: &std::path::Path,
env: HashMap<String, String>,
_timeout: Option<chrono::Duration>,
) -> Result<FixerResult, FixerError> {
pyo3::prepare_freethreaded_python();
use pyo3::import_exception;
use pyo3::prelude::*;
use pyo3::types::PyDict;
import_exception!(debmutate.reformatting, FormattingUnpreservable);
import_exception!(debian.changelog, ChangelogCreateError);
Python::with_gil(|py| {
let sys = py.import_bound("sys")?;
let os = py.import_bound("os")?;
let io = py.import_bound("io")?;
let fixer_module = py.import_bound("lintian_brush.fixer")?;
let old_env = os.getattr("environ")?.into_py(py);
let old_stderr = sys.getattr("stderr")?;
let old_stdout = sys.getattr("stdout")?;
let temp_stderr = io.call_method0("StringIO")?;
let temp_stdout = io.call_method0("StringIO")?;
sys.setattr("stderr", &temp_stderr)?;
sys.setattr("stdout", &temp_stdout)?;
os.setattr("environ", env)?;
let old_cwd = match os.call_method0("getcwd") {
Ok(cwd) => Some(cwd),
Err(_) => None,
};
os.call_method1("chdir", (basedir,))?;
let global_vars = PyDict::new_bound(py);
global_vars.set_item("__file__", path)?;
global_vars.set_item("__name__", "__main__")?;
let script_result = PyModule::from_code_bound(py, code, path.to_str().unwrap(), name);
let stdout = temp_stdout
.call_method0("getvalue")
.unwrap()
.extract::<String>()
.unwrap();
let mut stderr = temp_stderr
.call_method0("getvalue")
.unwrap()
.extract::<String>()
.unwrap();
os.setattr("environ", old_env).unwrap();
sys.setattr("stderr", old_stderr).unwrap();
sys.setattr("stdout", old_stdout).unwrap();
if let Some(cwd) = old_cwd {
os.call_method1("chdir", (cwd,))?;
}
fixer_module.call_method0("reset")?;
let retcode;
let description;
match script_result {
Ok(_) => {
retcode = 0;
description = stdout;
}
Err(e) => {
if e.is_instance_of::<FormattingUnpreservable>(py) {
return Err(FixerError::FormattingUnpreservable(
e.into_value(py).bind(py).getattr("path")?.extract()?,
));
} else if e.is_instance_of::<ChangelogCreateError>(py) {
return Err(FixerError::ChangelogCreate(
e.into_value(py).bind(py).get_item(0)?.extract()?,
));
} else if e.is_instance_of::<pyo3::exceptions::PyMemoryError>(py) {
return Err(FixerError::MemoryError);
} else if e.is_instance_of::<pyo3::exceptions::PySystemExit>(py) {
retcode = e.into_value(py).bind(py).getattr("code")?.extract()?;
description = stdout;
} else {
use pyo3::types::IntoPyDict;
let traceback = py.import_bound("traceback")?;
let traceback_io = io.call_method0("StringIO")?;
let kwargs = [("file", &traceback_io)].into_py_dict_bound(py);
traceback.call_method(
"print_exception",
(e.get_type_bound(py), &e, e.traceback_bound(py)),
Some(&kwargs),
)?;
let traceback_str =
traceback_io.call_method0("getvalue")?.extract::<String>()?;
stderr = format!("{}\n{}", stderr, traceback_str);
return Err(FixerError::ScriptFailed {
path: path.to_path_buf(),
exit_code: 1,
stderr,
});
}
}
}
if retcode == 2 {
Err(FixerError::NoChanges)
} else if retcode != 0 {
Err(FixerError::ScriptFailed {
path: path.to_path_buf(),
exit_code: retcode,
stderr,
})
} else {
Ok(parse_script_fixer_output(&description)?)
}
})
}
#[cfg(test)]
#[cfg(feature = "python")]
mod run_inline_python_fixer_tests {
fn setup() {
pyo3::Python::with_gil(|py| {
use pyo3::prelude::*;
let sys = py.import_bound("sys").unwrap();
let path = sys.getattr("path").unwrap();
let mut path: Vec<String> = path.extract().unwrap();
let extra_path =
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR").to_string() + "/../py")
.canonicalize()
.unwrap();
if !path.contains(&extra_path.to_string_lossy().to_string()) {
path.insert(0, extra_path.to_string_lossy().to_string());
sys.setattr("path", path).unwrap();
}
});
}
#[test]
fn test_no_changes() {
setup();
let td = tempfile::tempdir().unwrap();
let path = td.path().join("no_changes.py");
let result = super::run_inline_python_fixer(
&path,
"no_changes",
"import sys; sys.exit(2)",
td.path(),
std::collections::HashMap::new(),
None,
);
assert!(
matches!(result, Err(super::FixerError::NoChanges),),
"Result: {:?}",
result
);
}
#[test]
fn test_failed() {
setup();
let td = tempfile::tempdir().unwrap();
let path = td.path().join("no_changes.py");
let result = super::run_inline_python_fixer(
&path,
"some_changes",
"import sys; sys.exit(1)",
td.path(),
std::collections::HashMap::new(),
None,
);
assert!(
matches!(
result,
Err(super::FixerError::ScriptFailed { exit_code: 1, .. })
),
"Result: {:?}",
result
);
std::mem::drop(td);
}
#[test]
#[ignore]
fn test_timeout() {
setup();
let td = tempfile::tempdir().unwrap();
let path = td.path().join("no_changes.py");
let result = super::run_inline_python_fixer(
&path,
"some_changes",
"import time; time.sleep(10)",
td.path(),
std::collections::HashMap::new(),
Some(chrono::Duration::seconds(0)),
);
assert!(
matches!(result, Err(super::FixerError::Timeout { .. })),
"Result: {:?}",
result
);
}
}
#[cfg(feature = "python")]
impl Fixer for PythonScriptFixer {
fn name(&self) -> String {
self.name.clone()
}
fn path(&self) -> std::path::PathBuf {
self.path.clone()
}
fn lintian_tags(&self) -> Vec<String> {
self.lintian_tags.clone()
}
fn run(
&self,
basedir: &std::path::Path,
package: &str,
current_version: &Version,
preferences: &FixerPreferences,
timeout: Option<chrono::Duration>,
) -> Result<FixerResult, FixerError> {
let env = determine_env(package, current_version, preferences);
let code = std::fs::read_to_string(&self.path)
.map_err(|e| FixerError::Other(format!("Failed to read script: {}", e)))?;
run_inline_python_fixer(
&self.path,
self.name.as_str(),
code.as_str(),
basedir,
env,
timeout,
)
}
}
#[derive(Debug)]
pub enum FixerError {
NoChanges,
NoChangesAfterOverrides(Vec<LintianIssue>),
NotCertainEnough(Certainty, Option<Certainty>, Vec<LintianIssue>),
NotDebianPackage(std::path::PathBuf),
DescriptionMissing,
InvalidChangelog(std::path::PathBuf, String),
ScriptNotFound(std::path::PathBuf),
OutputParseError(OutputParseError),
OutputDecodeError(std::string::FromUtf8Error),
FailedPatchManipulation(String),
ChangelogCreate(String),
Timeout {
timeout: chrono::Duration,
},
ScriptFailed {
path: std::path::PathBuf,
exit_code: i32,
stderr: String,
},
FormattingUnpreservable(std::path::PathBuf),
GeneratedFile(std::path::PathBuf),
#[cfg(feature = "python")]
Python(pyo3::PyErr),
MemoryError,
Io(std::io::Error),
BrzError(Error),
Other(String),
}
impl From<debian_analyzer::editor::EditorError> for FixerError {
fn from(e: debian_analyzer::editor::EditorError) -> Self {
match e {
debian_analyzer::editor::EditorError::IoError(e) => e.into(),
debian_analyzer::editor::EditorError::BrzError(e) => e.into(),
debian_analyzer::editor::EditorError::GeneratedFile(p, _) => {
FixerError::GeneratedFile(p)
}
debian_analyzer::editor::EditorError::FormattingUnpreservable(p, _e) => {
FixerError::FormattingUnpreservable(p)
}
debian_analyzer::editor::EditorError::TemplateError(p, _e) => {
FixerError::GeneratedFile(p)
}
}
}
}
impl From<std::io::Error> for FixerError {
fn from(e: std::io::Error) -> Self {
FixerError::Io(e)
}
}
impl From<debian_changelog::Error> for FixerError {
fn from(e: debian_changelog::Error) -> Self {
match e {
debian_changelog::Error::Io(e) => FixerError::Io(e),
debian_changelog::Error::Parse(e) => FixerError::ChangelogCreate(e.to_string()),
}
}
}
impl From<ChangelogError> for FixerError {
fn from(e: ChangelogError) -> Self {
match e {
ChangelogError::NotDebianPackage(path) => FixerError::NotDebianPackage(path),
ChangelogError::Python(e) => FixerError::Other(e.to_string()),
}
}
}
impl From<Error> for FixerError {
fn from(e: Error) -> Self {
FixerError::BrzError(e)
}
}
impl From<OutputParseError> for FixerError {
fn from(e: OutputParseError) -> Self {
FixerError::OutputParseError(e)
}
}
#[cfg(feature = "python")]
impl From<pyo3::PyErr> for FixerError {
fn from(e: pyo3::PyErr) -> Self {
FixerError::Python(e)
}
}
impl std::fmt::Display for FixerError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
FixerError::NoChanges => write!(f, "No changes"),
FixerError::NoChangesAfterOverrides(_) => write!(f, "No changes after overrides"),
FixerError::OutputParseError(e) => write!(f, "Output parse error: {}", e),
FixerError::OutputDecodeError(e) => write!(f, "Output decode error: {}", e),
FixerError::ScriptNotFound(p) => write!(f, "Command not found: {}", p.display()),
FixerError::ChangelogCreate(m) => write!(f, "Changelog create error: {}", m),
FixerError::FormattingUnpreservable(p) => {
write!(f, "Formatting unpreservable for {}", p.display())
}
FixerError::ScriptFailed {
path,
exit_code,
stderr,
} => write!(
f,
"Script failed: {} (exit code {}) (stderr: {})",
path.display(),
exit_code,
stderr
),
FixerError::Other(s) => write!(f, "{}", s),
#[cfg(feature = "python")]
FixerError::Python(e) => write!(f, "{}", e),
FixerError::NotDebianPackage(p) => write!(f, "Not a Debian package: {}", p.display()),
FixerError::DescriptionMissing => {
write!(f, "Description missing")
}
FixerError::MemoryError => {
write!(f, "Memory error")
}
FixerError::NotCertainEnough(actual, minimum, _) => write!(
f,
"Not certain enough to fix (actual: {}, minimum : {:?})",
actual, minimum
),
FixerError::Io(e) => write!(f, "IO error: {}", e),
FixerError::FailedPatchManipulation(s) => write!(f, "Failed to manipulate patc: {}", s),
FixerError::BrzError(e) => write!(f, "Breezy error: {}", e),
FixerError::InvalidChangelog(p, s) => {
write!(f, "Invalid changelog {}: {}", p.display(), s)
}
FixerError::Timeout { timeout } => write!(f, "Timeout after {}", humantime::format_duration(timeout.to_std().unwrap())),
FixerError::GeneratedFile(p) => write!(f, "Generated file: {}", p.display()),
}
}
}
impl std::error::Error for FixerError {}
#[derive(Debug)]
pub struct ScriptFixer {
path: std::path::PathBuf,
name: String,
lintian_tags: Vec<String>,
}
impl ScriptFixer {
pub fn new(name: String, lintian_tags: Vec<String>, path: std::path::PathBuf) -> Self {
Self {
path,
name,
lintian_tags,
}
}
}
impl Fixer for ScriptFixer {
fn name(&self) -> String {
self.name.clone()
}
fn path(&self) -> std::path::PathBuf {
self.path.clone()
}
fn lintian_tags(&self) -> Vec<String> {
self.lintian_tags.clone()
}
fn run(
&self,
basedir: &std::path::Path,
package: &str,
current_version: &Version,
preferences: &FixerPreferences,
timeout: Option<chrono::Duration>,
) -> Result<FixerResult, FixerError> {
let env = determine_env(package, current_version, preferences);
use wait_timeout::ChildExt;
let mut cmd = Command::new(self.path.as_os_str());
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
cmd.current_dir(basedir);
for (key, value) in env.iter() {
cmd.env(key, value);
}
let mut child = cmd.spawn().map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => FixerError::ScriptNotFound(self.path.clone()),
_ => FixerError::Other(e.to_string()),
})?;
let status = if let Some(timeout) = timeout {
let std_timeout = timeout
.to_std()
.map_err(|e| FixerError::Other(e.to_string()))?;
let output = child
.wait_timeout(std_timeout)
.map_err(|e| FixerError::Other(e.to_string()))?;
if output.is_none() {
child.kill().map_err(|e| FixerError::Other(e.to_string()))?;
return Err(FixerError::Timeout { timeout });
}
output.unwrap()
} else {
child.wait().map_err(|e| FixerError::Other(e.to_string()))?
};
if !status.success() {
let mut stderr = String::new();
let mut stderr_buf = std::io::BufReader::new(child.stderr.as_mut().unwrap());
stderr_buf
.read_to_string(&mut stderr)
.map_err(|e| FixerError::Other(format!("Failed to read stderr: {}", e)))?;
if status.code() == Some(2) {
return Err(FixerError::NoChanges);
}
return Err(FixerError::ScriptFailed {
path: self.path.to_owned(),
exit_code: status.code().unwrap(),
stderr,
});
}
let mut stdout_buf = std::io::BufReader::new(child.stdout.as_mut().unwrap());
let mut stdout = String::new();
stdout_buf
.read_to_string(&mut stdout)
.map_err(FixerError::Io)?;
parse_script_fixer_output(&stdout).map_err(FixerError::OutputParseError)
}
}
#[derive(Debug, serde::Deserialize)]
struct DescEntry {
script: String,
#[serde(rename = "lintian-tags")]
lintian_tags: Option<Vec<String>>,
#[serde(rename = "force-subprocess")]
force_subprocess: Option<bool>,
}
#[derive(Debug)]
pub enum FixerDiscoverError {
Io(std::io::Error),
Yaml(serde_yaml::Error),
NoFixersDir,
}
impl From<std::io::Error> for FixerDiscoverError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
impl From<serde_yaml::Error> for FixerDiscoverError {
fn from(e: serde_yaml::Error) -> Self {
Self::Yaml(e)
}
}
impl std::fmt::Display for FixerDiscoverError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
FixerDiscoverError::Io(e) => write!(f, "IO error: {}", e),
FixerDiscoverError::Yaml(e) => write!(f, "YAML error: {}", e),
FixerDiscoverError::NoFixersDir => write!(f, "No fixers directory found"),
}
}
}
impl std::error::Error for FixerDiscoverError {}
pub fn read_desc_file<P: AsRef<std::path::Path>>(
path: P,
force_subprocess: bool,
) -> Result<impl Iterator<Item = Box<dyn Fixer>>, FixerDiscoverError> {
let file = File::open(path.as_ref())?;
let reader = BufReader::new(file);
let data: Vec<DescEntry> = serde_yaml::from_reader(reader)?;
let dirname = path.as_ref().parent().unwrap().to_owned();
let fixer_iter = data.into_iter().map(move |item| {
let script = item.script;
let lintian_tags = item.lintian_tags;
let force_subprocess = item.force_subprocess.unwrap_or(force_subprocess);
let name = std::path::Path::new(script.as_str())
.file_stem()
.and_then(|name| name.to_str())
.unwrap_or("");
let script_path = dirname.join(script.as_str());
load_fixer(
name.to_owned(),
lintian_tags.unwrap_or_default(),
script_path,
force_subprocess,
)
});
Ok(fixer_iter)
}
#[cfg(test)]
mod read_desc_file_tests {
#[test]
fn test_empty() {
let td = tempfile::tempdir().unwrap();
let path = td.path().join("empty.desc");
std::fs::write(&path, "").unwrap();
assert!(super::read_desc_file(&path, false)
.unwrap()
.next()
.is_none());
}
#[test]
fn test_single() {
let td = tempfile::tempdir().unwrap();
let path = td.path().join("single.desc");
std::fs::write(
&path,
r#"---
- script: foo.sh
lintian-tags:
- bar
- baz
"#,
)
.unwrap();
let script_path = td.path().join("foo.sh");
std::fs::write(script_path, "#!/bin/sh\n").unwrap();
let fixer = super::read_desc_file(&path, false).unwrap().next().unwrap();
assert_eq!(fixer.name(), "foo");
assert_eq!(fixer.lintian_tags(), vec!["bar", "baz"]);
}
}
fn load_fixer(
name: String,
tags: Vec<String>,
script_path: std::path::PathBuf,
#[allow(dead_code)] force_subprocess: bool,
) -> Box<dyn Fixer> {
#[cfg(feature = "python")]
if script_path
.extension()
.map(|ext| ext == "py")
.unwrap_or(false)
&& !force_subprocess
{
return Box::new(PythonScriptFixer::new(name, tags, script_path));
}
Box::new(ScriptFixer::new(name, tags, script_path))
}
pub fn available_lintian_fixers(
fixers_dir: Option<&std::path::Path>,
force_subprocess: Option<bool>,
) -> Result<impl Iterator<Item = Box<dyn Fixer>>, FixerDiscoverError> {
let fixers_dir = if let Some(fixers_dir) = fixers_dir {
fixers_dir.to_path_buf()
} else {
let system_path = find_fixers_dir();
if let Some(system_path) = system_path {
system_path
} else {
return Err(FixerDiscoverError::NoFixersDir);
}
};
let mut fixers = Vec::new();
for entry in std::fs::read_dir(fixers_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().map(|ext| ext == "desc").unwrap_or(false) {
let fixer_iter = read_desc_file(&path, force_subprocess.unwrap_or(false))?;
fixers.extend(fixer_iter);
}
}
Ok(fixers.into_iter())
}
#[derive(Debug, PartialEq, Eq)]
pub struct UnknownFixer(pub String);
impl std::fmt::Display for UnknownFixer {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Unknown fixer: {}", self.0)
}
}
impl std::error::Error for UnknownFixer {}
pub fn select_fixers(
fixers: Vec<Box<dyn Fixer>>,
names: Option<&[&str]>,
exclude: Option<&[&str]>,
) -> Result<Vec<Box<dyn Fixer>>, UnknownFixer> {
let mut select_set = names.map(|names| names.iter().cloned().collect::<HashSet<_>>());
let mut exclude_set = exclude.map(|exclude| exclude.iter().cloned().collect::<HashSet<_>>());
let mut ret = vec![];
for f in fixers.into_iter() {
if let Some(select_set) = select_set.as_mut() {
if !select_set.remove(f.name().as_str()) {
if let Some(exclude_set) = exclude_set.as_mut() {
exclude_set.remove(f.name().as_str());
}
continue;
}
}
if let Some(exclude_set) = exclude_set.as_mut() {
if exclude_set.remove(f.name().as_str()) {
continue;
}
}
ret.push(f);
}
if let Some(select_set) = select_set.filter(|x| !x.is_empty()) {
Err(UnknownFixer(select_set.iter().next().unwrap().to_string()))
} else if let Some(exclude_set) = exclude_set.filter(|x| !x.is_empty()) {
Err(UnknownFixer(exclude_set.iter().next().unwrap().to_string()))
} else {
Ok(ret)
}
}
#[cfg(test)]
mod select_fixers_tests {
use super::*;
#[derive(Debug)]
struct DummyFixer<'a> {
name: &'a str,
tags: Vec<&'a str>,
}
impl DummyFixer<'_> {
fn new<'a>(name: &'a str, tags: &[&'a str]) -> DummyFixer<'a> {
DummyFixer {
name,
tags: tags.to_vec(),
}
}
}
impl<'a> Fixer for DummyFixer<'a> {
fn name(&self) -> String {
self.name.to_string()
}
fn path(&self) -> std::path::PathBuf {
unimplemented!()
}
fn lintian_tags(&self) -> Vec<String> {
self.tags.iter().map(|s| s.to_string()).collect::<Vec<_>>()
}
fn run(
&self,
_basedir: &std::path::Path,
_package: &str,
_current_version: &Version,
_preferences: &FixerPreferences,
_timeout: Option<chrono::Duration>,
) -> Result<FixerResult, FixerError> {
unimplemented!()
}
}
#[test]
fn test_exists() {
assert_eq!(
Ok(vec!["dummy1".to_string()]),
select_fixers(
vec![
Box::new(DummyFixer::new("dummy1", &["some-tag"])),
Box::new(DummyFixer::new("dummy2", &["other-tag"])),
],
Some(vec!["dummy1"].as_slice()),
None
)
.map(|m| m.into_iter().map(|f| f.name()).collect::<Vec<_>>())
);
}
#[test]
fn test_missing() {
assert!(select_fixers(
vec![
Box::new(DummyFixer::new("dummy1", &["some-tag"])),
Box::new(DummyFixer::new("dummy2", &["other-tag"])),
],
Some(vec!["other"].as_slice()),
None
)
.is_err());
}
#[test]
fn test_exclude_missing() {
assert!(select_fixers(
vec![
Box::new(DummyFixer::new("dummy1", &["some-tag"])),
Box::new(DummyFixer::new("dummy2", &["other-tag"])),
],
Some(vec!["dummy"].as_slice()),
Some(vec!["some-other"].as_slice())
)
.is_err());
}
#[test]
fn test_exclude() {
assert_eq!(
Ok(vec!["dummy1".to_string()]),
select_fixers(
vec![
Box::new(DummyFixer::new("dummy1", &["some-tag"])),
Box::new(DummyFixer::new("dummy2", &["other-tag"])),
],
Some(vec!["dummy1"].as_slice()),
Some(vec!["dummy2"].as_slice())
)
.map(|m| m.into_iter().map(|f| f.name()).collect::<Vec<_>>())
);
}
}
pub const DEFAULT_VALUE_LINTIAN_BRUSH_ADDON_ONLY: i32 = 10;
pub const DEFAULT_VALUE_LINTIAN_BRUSH: i32 = 50;
pub const LINTIAN_BRUSH_TAG_VALUES: [(&str, i32); 1] = [("trailing-whitespace", 0)];
pub const DEFAULT_ADDON_FIXERS: &[&str] = &[
"debian-changelog-line-too-long",
"trailing-whitespace",
"out-of-date-standards-version",
"package-uses-old-debhelper-compat-version",
"public-upstream-key-not-minimal",
];
pub const LINTIAN_BRUSH_TAG_DEFAULT_VALUE: i32 = 5;
pub fn calculate_value(tags: &[&str]) -> i32 {
if tags.is_empty() {
return 0;
}
let default_addon_fixers: HashSet<&str> = DEFAULT_ADDON_FIXERS.iter().cloned().collect();
let tag_set: HashSet<&str> = tags.iter().cloned().collect();
if tag_set.is_subset(&default_addon_fixers) {
return DEFAULT_VALUE_LINTIAN_BRUSH_ADDON_ONLY;
}
let mut value = DEFAULT_VALUE_LINTIAN_BRUSH;
for tag in tags {
if let Some(tag_value) = LINTIAN_BRUSH_TAG_VALUES.iter().find(|(t, _)| t == tag) {
value += tag_value.1;
} else {
value += LINTIAN_BRUSH_TAG_DEFAULT_VALUE;
}
}
value
}
pub fn data_file_path(
name: &str,
check: impl Fn(&std::path::Path) -> bool,
) -> Option<std::path::PathBuf> {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path = path.join("..").join(name);
if check(&path) {
return Some(path);
}
#[cfg(feature = "python")]
pyo3::prepare_freethreaded_python();
#[cfg(feature = "python")]
if let Some(path) = pyo3::Python::with_gil(|py| {
use pyo3::prelude::*;
let pkg_resources = py.import_bound("pkg_resources").unwrap();
if let Ok(path) = pkg_resources.call_method1(
"resource_filename",
("lintian_brush", format!("lintian-brush/{}", name)),
) {
if let Ok(path) = path.extract::<std::path::PathBuf>() {
if check(path.as_path()) {
return Some(path);
}
}
}
None
}) {
return Some(path);
}
let base_paths = &["/usr/share/lintian-brush", "/usr/local/share/lintian-brush"];
for base_path in base_paths {
let path = std::path::Path::new(base_path).join(name);
if check(&path) {
return Some(path);
}
}
None
}
pub fn find_fixers_dir() -> Option<std::path::PathBuf> {
data_file_path("fixers", |path| path.is_dir())
}
pub fn run_lintian_fixer(
local_tree: &WorkingTree,
fixer: &dyn Fixer,
committer: Option<&str>,
mut update_changelog: impl FnMut() -> bool,
preferences: &FixerPreferences,
dirty_tracker: &mut Option<DirtyTreeTracker>,
subpath: &std::path::Path,
timestamp: Option<chrono::naive::NaiveDateTime>,
basis_tree: Option<&dyn Tree>,
changes_by: Option<&str>,
timeout: Option<chrono::Duration>,
) -> Result<(FixerResult, String), FixerError> {
let changes_by = changes_by.unwrap_or("lintian-brush");
let changelog_path = subpath.join("debian/changelog");
let r = match local_tree.get_file(changelog_path.as_path()) {
Ok(f) => f,
Err(Error::NoSuchFile(_pb)) => {
return Err(FixerError::NotDebianPackage(
local_tree.abspath(subpath).unwrap(),
));
}
Err(e) => return Err(FixerError::Other(e.to_string())),
};
let cl = ChangeLog::read(r)?;
let first_entry = if let Some(entry) = cl.entries().next() {
entry
} else {
return Err(FixerError::InvalidChangelog(
local_tree.abspath(subpath).unwrap(),
"No entries in changelog".to_string(),
));
};
let package = first_entry.package().unwrap();
let current_version: Version =
if first_entry.distributions().as_deref().unwrap() == vec!["UNRELEASED"] {
first_entry.version().unwrap()
} else {
let mut version = first_entry.version().unwrap();
version.increment_debian();
version
};
let mut _bt = None;
let basis_tree: &dyn Tree = if let Some(basis_tree) = basis_tree {
basis_tree
} else {
_bt = Some(local_tree.basis_tree().unwrap());
_bt.as_ref().unwrap()
};
let make_changes = |basedir: &std::path::Path| -> Result<_, FixerError> {
log::debug!("Running fixer {:?}", fixer);
let result = fixer.run(
basedir,
package.as_str(),
¤t_version,
preferences,
timeout,
)?;
if let Some(certainty) = result.certainty {
if !certainty_sufficient(certainty, preferences.minimum_certainty) {
return Err(FixerError::NotCertainEnough(
certainty,
preferences.minimum_certainty,
result.overridden_lintian_issues,
));
}
}
Ok(result)
};
let (mut result, changes, mut specific_files) = match apply_or_revert(
local_tree,
subpath,
basis_tree,
dirty_tracker.as_mut(),
make_changes,
) {
Ok(r) => {
if r.0.description.is_empty() {
return Err(FixerError::DescriptionMissing);
}
r
}
Err(ApplyError::NoChanges(r)) => {
if r.overridden_lintian_issues.is_empty() {
return Err(FixerError::NoChanges);
} else {
return Err(FixerError::NoChangesAfterOverrides(
r.overridden_lintian_issues,
));
}
}
Err(ApplyError::BrzError(e)) => {
return Err(e.into());
}
Err(ApplyError::CallbackError(e)) => {
return Err(e);
}
};
let lines = result.description.split('\n').collect::<Vec<_>>();
let mut summary = lines[0].to_string();
let details = lines
.iter()
.skip(1)
.take_while(|l| !l.is_empty())
.collect::<Vec<_>>();
if has_non_debian_changes(changes.as_slice(), subpath)
&& current_version.debian_revision.is_some()
{
let (patch_name, updated_specific_files) = match upstream_changes_to_patch(
local_tree,
basis_tree,
dirty_tracker.as_mut(),
subpath,
&result
.patch_name
.as_deref()
.map_or_else(|| fixer.name(), |n| n.to_string()),
result.description.as_str(),
timestamp.map(|t| t.date()),
) {
Ok(r) => r,
Err(e) => {
reset_tree_with_dirty_tracker(
local_tree,
Some(basis_tree),
Some(subpath),
dirty_tracker.as_mut(),
)
.map_err(|e| FixerError::Other(e.to_string()))?;
return Err(FixerError::FailedPatchManipulation(e.to_string()));
}
};
specific_files = Some(updated_specific_files);
summary = format!("Add patch {}: {}", patch_name, summary);
}
let update_changelog = if debian_analyzer::changelog::only_changes_last_changelog_block(
local_tree,
basis_tree,
changelog_path.as_path(),
changes.iter(),
)? {
false
} else {
update_changelog()
};
if update_changelog {
let mut entry = vec![summary.as_str()];
entry.extend(details);
add_changelog_entry(local_tree, changelog_path.as_path(), entry.as_slice())?;
if let Some(specific_files) = specific_files.as_mut() {
specific_files.push(changelog_path);
}
}
let mut description = format!("{}\n", result.description);
description.push('\n');
description.push_str(format!("Changes-By: {}\n", changes_by).as_str());
for tag in result.fixed_lintian_tags() {
description.push_str(format!("Fixes: lintian: {}\n", tag).as_str());
description
.push_str(format!("See-also: https://lintian.debian.org/tags/{}.html\n", tag).as_str());
}
let committer = committer.map_or_else(|| get_committer(local_tree), |c| c.to_string());
let specific_files_ref = specific_files
.as_ref()
.map(|fs| fs.iter().map(|p| p.as_path()).collect::<Vec<_>>());
let mut builder = local_tree
.build_commit()
.message(description.as_str())
.allow_pointless(false)
.committer(committer.as_str());
if let Some(specific_files_ref) = specific_files_ref.as_ref() {
builder = builder.specific_files(specific_files_ref);
}
let revid = builder.commit().map_err(|e| match e {
Error::PointlessCommit => FixerError::NoChanges,
Error::NoWhoami => FixerError::Other("No committer specified".to_string()),
e => FixerError::Other(e.to_string()),
})?;
result.revision_id = Some(revid);
Ok((result, summary))
}
#[derive(Debug)]
pub enum OverallError {
NotDebianPackage(std::path::PathBuf),
WorkspaceDirty(std::path::PathBuf),
ChangelogCreate(String),
InvalidChangelog(std::path::PathBuf, String),
BrzError(Error),
IoError(std::io::Error),
Other(String),
#[cfg(feature = "python")]
Python(pyo3::PyErr),
}
#[cfg(feature = "python")]
impl From<pyo3::PyErr> for OverallError {
fn from(e: pyo3::PyErr) -> Self {
OverallError::Python(e)
}
}
impl std::fmt::Display for OverallError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
OverallError::NotDebianPackage(path) => {
write!(f, "Not a Debian package: {}", path.display())
}
OverallError::WorkspaceDirty(path) => {
write!(f, "Workspace is dirty: {}", path.display())
}
OverallError::ChangelogCreate(m) => {
write!(f, "Failed to create changelog entry: {}", m)
}
#[cfg(feature = "python")]
OverallError::Python(e) => write!(f, "{}", e),
OverallError::Other(e) => write!(f, "{}", e),
OverallError::BrzError(e) => write!(f, "{}", e),
OverallError::IoError(e) => write!(f, "{}", e),
OverallError::InvalidChangelog(path, e) => {
write!(f, "Invalid changelog at {}: {}", path.display(), e)
}
}
}
}
impl std::error::Error for OverallError {}
pub fn run_lintian_fixers(
local_tree: &WorkingTree,
fixers: &[Box<dyn Fixer>],
mut update_changelog: Option<impl FnMut() -> bool>,
verbose: bool,
committer: Option<&str>,
preferences: &FixerPreferences,
use_dirty_tracker: Option<bool>,
subpath: Option<&std::path::Path>,
changes_by: Option<&str>,
timeout: Option<chrono::Duration>,
) -> Result<ManyResult, OverallError> {
let subpath = subpath.unwrap_or_else(|| std::path::Path::new(""));
let mut basis_tree = local_tree.basis_tree().unwrap();
check_clean_tree(local_tree, &basis_tree, subpath).map_err(|e| match e {
Error::WorkspaceDirty(p) => OverallError::WorkspaceDirty(p),
e => OverallError::Other(e.to_string()),
})?;
let mut changelog_behaviour = None;
let mut update_changelog = || {
if let Some(update_changelog) = update_changelog.as_mut() {
return update_changelog();
}
let debian_path = subpath.join("debian");
let cb = determine_update_changelog(local_tree, debian_path.as_path());
changelog_behaviour = Some(cb);
changelog_behaviour.as_ref().unwrap().update_changelog
};
let mut ret = ManyResult::new();
let pb = ProgressBar::new(fixers.len() as u64);
#[cfg(test)]
pb.set_draw_target(indicatif::ProgressDrawTarget::hidden());
let mut dirty_tracker = if use_dirty_tracker.unwrap_or(true) {
Some(DirtyTreeTracker::new_in_subpath(
local_tree.clone(),
subpath,
))
} else {
None
};
for fixer in fixers {
pb.set_message(format!("Running fixer {}", fixer.name()));
let start = std::time::SystemTime::now();
if let Some(dirty_tracker) = dirty_tracker.as_mut() {
dirty_tracker.mark_clean();
}
pb.inc(1);
match run_lintian_fixer(
local_tree,
fixer.as_ref(),
committer,
&mut update_changelog,
preferences,
&mut dirty_tracker,
subpath,
None,
Some(&basis_tree),
changes_by,
timeout,
) {
Err(e) => match e {
FixerError::NotDebianPackage(path) => {
return Err(OverallError::NotDebianPackage(path));
}
FixerError::ChangelogCreate(m) => {
return Err(OverallError::ChangelogCreate(m));
}
FixerError::OutputParseError(ref _e) => {
ret.failed_fixers.insert(fixer.name(), e.to_string());
if verbose {
log::info!("Fixer {} failed to parse output.", fixer.name());
}
continue;
}
FixerError::DescriptionMissing => {
ret.failed_fixers.insert(fixer.name(), e.to_string());
if verbose {
log::info!(
"Fixer {} failed because description is missing.",
fixer.name()
);
}
continue;
}
FixerError::OutputDecodeError(ref _e) => {
ret.failed_fixers.insert(fixer.name(), e.to_string());
if verbose {
log::info!("Fixer {} failed to decode output.", fixer.name());
}
continue;
}
FixerError::FormattingUnpreservable(path) => {
ret.formatting_unpreservable
.insert(fixer.name(), path.clone());
if verbose {
log::info!(
"Fixer {} was unable to preserve formatting of {}.",
fixer.name(),
path.display()
);
}
continue;
}
FixerError::GeneratedFile(p) => {
ret.failed_fixers
.insert(fixer.name(), format!("Generated file: {}", p.display()));
if verbose {
log::info!(
"Fixer {} encountered generated file {}",
fixer.name(),
p.display()
);
}
}
FixerError::ScriptNotFound(ref p) => {
ret.failed_fixers.insert(fixer.name(), e.to_string());
if verbose {
log::info!("Fixer {} ({}) not found.", fixer.name(), p.display());
}
continue;
}
FixerError::ScriptFailed { .. } => {
ret.failed_fixers.insert(fixer.name(), e.to_string());
if verbose {
log::info!("Fixer {} failed to run.", fixer.name());
eprintln!("{}", e);
}
continue;
}
FixerError::MemoryError => {
ret.failed_fixers.insert(fixer.name(), e.to_string());
if verbose {
log::info!("Ran out of memory while running fixer {}.", fixer.name());
}
continue;
}
FixerError::BrzError(e) => {
return Err(OverallError::BrzError(e));
}
FixerError::Io(e) => {
return Err(OverallError::IoError(e));
}
FixerError::NotCertainEnough(actual_certainty, minimum_certainty, _overrides) => {
if verbose {
let duration = std::time::SystemTime::now().duration_since(start).unwrap();
log::info!(
"Fixer {} made changes but not high enough certainty (was {}, needed {}). (took: {:2}s)",
fixer.name(),
actual_certainty,
minimum_certainty.map_or("default".to_string(), |c| c.to_string()),
duration.as_secs_f32(),
);
}
continue;
}
FixerError::FailedPatchManipulation(ref reason) => {
if verbose {
log::info!("Unable to manipulate upstream patches: {}", reason);
}
ret.failed_fixers.insert(fixer.name(), e.to_string());
continue;
}
FixerError::NoChanges => {
if verbose {
let duration = std::time::SystemTime::now().duration_since(start).unwrap();
log::info!(
"Fixer {} made no changes. (took: {:2}s)",
fixer.name(),
duration.as_secs_f32(),
);
}
continue;
}
FixerError::NoChangesAfterOverrides(os) => {
if verbose {
let duration = std::time::SystemTime::now().duration_since(start).unwrap();
log::info!(
"Fixer {} made no changes. (took: {:2}s)",
fixer.name(),
duration.as_secs_f32(),
);
}
ret.overridden_lintian_issues.extend(os);
continue;
}
#[cfg(feature = "python")]
FixerError::Python(ref ep) => {
if verbose {
log::info!("Fixer {} failed: {}", fixer.name(), ep);
}
ret.failed_fixers.insert(fixer.name(), e.to_string());
continue;
}
FixerError::Other(ref em) => {
if verbose {
log::info!("Fixer {} failed: {}", fixer.name(), em);
}
ret.failed_fixers.insert(fixer.name(), e.to_string());
continue;
}
FixerError::InvalidChangelog(path, reason) => {
return Err(OverallError::InvalidChangelog(path, reason));
}
FixerError::Timeout { timeout } => {
if verbose {
log::info!("Fixer {} timed out after {}.", fixer.name(), timeout);
}
ret.failed_fixers.insert(fixer.name(), e.to_string());
continue;
}
},
Ok((result, summary)) => {
if verbose {
let duration = std::time::SystemTime::now().duration_since(start).unwrap();
log::info!(
"Fixer {} made changes. (took {:2}s)",
fixer.name(),
duration.as_secs_f32(),
);
}
ret.success.push((result, summary));
basis_tree = local_tree.basis_tree().unwrap();
}
}
}
pb.finish();
ret.changelog_behaviour = changelog_behaviour;
Ok(ret)
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
pub struct ManyResult {
#[serde(rename = "applied")]
pub success: Vec<(FixerResult, String)>,
#[serde(rename = "failed")]
pub failed_fixers: std::collections::HashMap<String, String>,
pub changelog_behaviour: Option<ChangelogBehaviour>,
#[serde(skip)]
pub overridden_lintian_issues: Vec<LintianIssue>,
#[serde(skip)]
pub formatting_unpreservable: std::collections::HashMap<String, std::path::PathBuf>,
}
impl ManyResult {
pub fn tags_count(&self) -> HashMap<&str, u32> {
self.success
.iter()
.fold(HashMap::new(), |mut acc, (r, _summary)| {
for tag in r.fixed_lintian_tags() {
*acc.entry(tag).or_insert(0) += 1;
}
acc
})
}
pub fn value(&self) -> i32 {
let tags = self
.success
.iter()
.flat_map(|(r, _summary)| r.fixed_lintian_tags())
.collect::<Vec<_>>();
calculate_value(tags.as_slice())
}
pub fn minimum_success_certainty(&self) -> Certainty {
min_certainty(
self.success
.iter()
.filter_map(|(r, _summary)| r.certainty)
.collect::<Vec<_>>()
.as_slice(),
)
.unwrap_or(Certainty::Certain)
}
pub fn new() -> Self {
Self {
success: Vec::new(),
failed_fixers: std::collections::HashMap::new(),
changelog_behaviour: None,
overridden_lintian_issues: Vec::new(),
formatting_unpreservable: std::collections::HashMap::new(),
}
}
}
fn has_non_debian_changes(changes: &[TreeChange], subpath: &std::path::Path) -> bool {
let debian_path = subpath.join("debian");
changes.iter().any(|change| {
[change.path.0.as_deref(), change.path.1.as_deref()]
.into_iter()
.flatten()
.any(|path| !path.starts_with(&debian_path))
})
}
#[derive(Debug)]
struct FailedPatchManipulation(String);
impl std::fmt::Display for FailedPatchManipulation {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "Failed to manipulate patches: {}", self.0)
}
}
impl std::error::Error for FailedPatchManipulation {}
fn upstream_changes_to_patch(
local_tree: &WorkingTree,
basis_tree: &dyn Tree,
dirty_tracker: Option<&mut DirtyTreeTracker>,
subpath: &std::path::Path,
patch_name: &str,
description: &str,
timestamp: Option<chrono::naive::NaiveDate>,
) -> Result<(String, Vec<std::path::PathBuf>), FailedPatchManipulation> {
use debian_analyzer::patches::{
move_upstream_changes_to_patch, read_quilt_patches, tree_patches_directory,
};
let patches_directory = tree_patches_directory(local_tree, subpath);
let quilt_patches =
read_quilt_patches(local_tree, patches_directory.as_path()).collect::<Vec<_>>();
if !quilt_patches.is_empty() {
return Err(FailedPatchManipulation(
"Creating patch on top of existing quilt patches not supported.".to_string(),
));
}
log::debug!("Moving upstream changes to patch {}", patch_name);
let (specific_files, patch_name) = match move_upstream_changes_to_patch(
local_tree,
basis_tree,
subpath,
patch_name,
description,
dirty_tracker,
timestamp,
) {
Ok(r) => r,
Err(e) => {
return Err(FailedPatchManipulation(e.to_string()));
}
};
Ok((patch_name, specific_files))
}
fn note_changelog_policy(policy: bool, msg: &str) {
lazy_static::lazy_static! {
static ref CHANGELOG_POLICY_NOTED: std::sync::Mutex<bool> = std::sync::Mutex::new(false);
}
if let Ok(mut policy_noted) = CHANGELOG_POLICY_NOTED.lock() {
if !*policy_noted {
let extra = if policy {
"Specify --no-update-changelog to override."
} else {
"Specify --update-changelog to override."
};
log::info!("{} {}", msg, extra);
}
*policy_noted = true;
}
}
pub fn determine_update_changelog(
local_tree: &WorkingTree,
debian_path: &std::path::Path,
) -> ChangelogBehaviour {
let changelog_path = debian_path.join("changelog");
let cl = match local_tree.get_file(changelog_path.as_path()) {
Ok(f) => ChangeLog::read(f).unwrap(),
Err(Error::NoSuchFile(_)) => {
return ChangelogBehaviour {
update_changelog: false,
explanation: "No changelog found".to_string(),
};
}
Err(e) => {
panic!("Error reading changelog: {}", e);
}
};
let behaviour = guess_update_changelog(local_tree, debian_path, Some(cl));
let behaviour = if let Some(behaviour) = behaviour {
note_changelog_policy(behaviour.update_changelog, behaviour.explanation.as_str());
behaviour
} else {
ChangelogBehaviour {
update_changelog: true,
explanation: "Assuming changelog should be updated".to_string(),
}
};
behaviour
}
#[cfg(test)]
mod tests {
use super::*;
use breezyshim::controldir::{create_standalone_workingtree, ControlDirFormat};
use breezyshim::tree::{MutableTree, WorkingTree};
use std::path::Path;
pub const COMMITTER: &str = "Testsuite <lintian-brush@example.com>";
mod test_run_lintian_fixer {
use super::*;
#[derive(Debug)]
struct DummyFixer {
name: String,
lintian_tags: Vec<String>,
}
impl DummyFixer {
fn new(name: &str, lintian_tags: &[&str]) -> Self {
Self {
name: name.to_string(),
lintian_tags: lintian_tags.iter().map(|t| t.to_string()).collect(),
}
}
}
impl Fixer for DummyFixer {
fn name(&self) -> String {
self.name.clone()
}
fn path(&self) -> std::path::PathBuf {
std::path::PathBuf::from("/dev/null")
}
fn lintian_tags(&self) -> Vec<String> {
self.lintian_tags.clone()
}
fn run(
&self,
basedir: &std::path::Path,
package: &str,
_current_version: &Version,
_preferences: &FixerPreferences,
_timeout: Option<chrono::Duration>,
) -> Result<FixerResult, FixerError> {
std::fs::write(basedir.join("debian/control"), "a new line\n").unwrap();
Ok(FixerResult {
description: "Fixed some tag.\nExtended description.".to_string(),
patch_name: None,
certainty: Some(Certainty::Certain),
fixed_lintian_issues: vec![LintianIssue {
tag: Some("some-tag".to_string()),
package: Some(package.to_string()),
info: None,
package_type: Some(PackageType::Source),
}],
overridden_lintian_issues: vec![],
revision_id: None,
})
}
}
#[derive(Debug)]
struct FailingFixer {
name: String,
lintian_tags: Vec<String>,
}
impl FailingFixer {
fn new(name: &str, lintian_tags: &[&str]) -> Self {
Self {
name: name.to_string(),
lintian_tags: lintian_tags.iter().map(|t| t.to_string()).collect(),
}
}
}
impl Fixer for FailingFixer {
fn name(&self) -> String {
self.name.clone()
}
fn path(&self) -> std::path::PathBuf {
std::path::PathBuf::from("/dev/null")
}
fn lintian_tags(&self) -> Vec<String> {
self.lintian_tags.clone()
}
fn run(
&self,
basedir: &std::path::Path,
_package: &str,
_current_version: &Version,
_preferences: &FixerPreferences,
_timeout: Option<chrono::Duration>,
) -> Result<FixerResult, FixerError> {
std::fs::write(basedir.join("debian/foo"), "blah").unwrap();
std::fs::write(basedir.join("debian/control"), "foo\n").unwrap();
Err(FixerError::ScriptFailed {
stderr: "Not successful".to_string(),
path: std::path::PathBuf::from("/dev/null"),
exit_code: 1,
})
}
}
fn setup(version: Option<&str>) -> (tempfile::TempDir, WorkingTree) {
let version = version.unwrap_or("0.1");
let td = tempfile::tempdir().unwrap();
let tree =
create_standalone_workingtree(td.path(), &ControlDirFormat::default()).unwrap();
tree.mkdir(std::path::Path::new("debian")).unwrap();
std::fs::write(
td.path().join("debian/control"),
r#""Source: blah
Vcs-Git: https://example.com/blah
Testsuite: autopkgtest
Binary: blah
Arch: all
"#,
)
.unwrap();
tree.add(&[std::path::Path::new("debian/control")]).unwrap();
std::fs::write(
td.path().join("debian/changelog"),
format!(
r#"blah ({}) UNRELEASED; urgency=medium
* Initial release. (Closes: #911016)
-- Blah <example@debian.org> Sat, 13 Oct 2018 11:21:39 +0100
"#,
version
),
)
.unwrap();
tree.add(&[std::path::Path::new("debian/changelog")])
.unwrap();
tree.build_commit()
.message("Initial thingy.")
.committer(COMMITTER)
.commit()
.unwrap();
(td, tree)
}
#[test]
fn test_fails() {
let (td, tree) = setup(None);
let lock = tree.lock_write().unwrap();
let result = run_lintian_fixers(
&tree,
&[Box::new(FailingFixer::new("fail", &["some-tag"]))],
Some(|| false),
false,
None,
&FixerPreferences::default(),
None,
None,
None,
None,
)
.unwrap();
std::mem::drop(lock);
assert_eq!(0, result.success.len());
assert_eq!(1, result.failed_fixers.len());
let fixer = result.failed_fixers.get("fail").unwrap();
assert!(fixer.contains("Not successful"));
let lock = tree.lock_read().unwrap();
assert_eq!(
Vec::<breezyshim::tree::TreeChange>::new(),
tree.iter_changes(&tree.basis_tree().unwrap(), None, None, None)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
);
std::mem::drop(lock);
std::mem::drop(td);
}
#[test]
fn test_not_debian_tree() {
let (td, tree) = setup(None);
tree.remove(&[(std::path::Path::new("debian/changelog"))])
.unwrap();
std::fs::remove_file(td.path().join("debian/changelog")).unwrap();
tree.build_commit()
.message("not a debian dir")
.committer(COMMITTER)
.commit()
.unwrap();
let lock = tree.lock_write().unwrap();
assert!(matches!(
run_lintian_fixers(
&tree,
&[Box::new(DummyFixer::new("dummy", &["some-tag"][..]))],
Some(|| false),
false,
None,
&FixerPreferences::default(),
None,
None,
None,
None,
),
Err(OverallError::NotDebianPackage(_))
));
std::mem::drop(lock);
std::mem::drop(td);
}
#[test]
fn test_simple_modify() {
let (td, tree) = setup(None);
let lock = tree.lock_write().unwrap();
let result = run_lintian_fixers(
&tree,
&[Box::new(DummyFixer::new("dummy", &["some-tag"]))],
Some(|| false),
false,
Some(COMMITTER),
&FixerPreferences::default(),
None,
None,
None,
None,
)
.unwrap();
let revid = tree.last_revision().unwrap();
std::mem::drop(lock);
assert_eq!(
vec![(
FixerResult::new(
"Fixed some tag.\nExtended description.".to_string(),
None,
Some(Certainty::Certain),
None,
Some(revid),
vec![LintianIssue {
tag: Some("some-tag".to_string()),
package: Some("blah".to_string()),
info: None,
package_type: Some(PackageType::Source),
}],
None,
),
"Fixed some tag.".to_string()
)],
result.success,
);
assert_eq!(maplit::hashmap! {}, result.failed_fixers);
assert_eq!(2, tree.branch().revno());
let lines = tree
.get_file_lines(std::path::Path::new("debian/control"))
.unwrap();
assert_eq!(lines.last().unwrap(), &b"a new line\n".to_vec());
std::mem::drop(td);
}
#[test]
fn test_simple_modify_too_uncertain() {
let (td, tree) = setup(None);
#[derive(Debug)]
struct UncertainFixer {
name: String,
lintian_tags: Vec<String>,
}
impl UncertainFixer {
fn new(name: &str, lintian_tags: &[&str]) -> Self {
Self {
name: name.to_string(),
lintian_tags: lintian_tags.iter().map(|t| t.to_string()).collect(),
}
}
}
impl Fixer for UncertainFixer {
fn name(&self) -> String {
self.name.clone()
}
fn path(&self) -> std::path::PathBuf {
std::path::PathBuf::from("/dev/null")
}
fn lintian_tags(&self) -> Vec<String> {
self.lintian_tags.clone()
}
fn run(
&self,
basedir: &std::path::Path,
_package: &str,
_current_version: &Version,
_preferences: &FixerPreferences,
_timeout: Option<chrono::Duration>,
) -> Result<FixerResult, FixerError> {
std::fs::write(basedir.join("debian/somefile"), "test").unwrap();
Ok(FixerResult {
description: "Renamed a file.".to_string(),
patch_name: None,
certainty: Some(Certainty::Possible),
fixed_lintian_issues: vec![],
overridden_lintian_issues: vec![],
revision_id: None,
})
}
}
let lock_write = tree.lock_write().unwrap();
let result = run_lintian_fixer(
&tree,
&UncertainFixer::new("dummy", &["some-tag"]),
Some(COMMITTER),
|| false,
&FixerPreferences {
minimum_certainty: Some(Certainty::Certain),
..Default::default()
},
&mut None,
Path::new(""),
None,
None,
None,
None,
);
assert!(
matches!(result, Err(FixerError::NotCertainEnough(..))),
"{:?}",
result
);
assert_eq!(1, tree.branch().revno());
std::mem::drop(lock_write);
std::mem::drop(td);
}
#[test]
fn test_simple_modify_acceptably_uncertain() {
let (td, tree) = setup(None);
#[derive(Debug)]
struct UncertainFixer {
name: String,
lintian_tags: Vec<String>,
}
impl UncertainFixer {
fn new(name: &str, lintian_tags: &[&str]) -> Self {
Self {
name: name.to_string(),
lintian_tags: lintian_tags.iter().map(|t| t.to_string()).collect(),
}
}
}
impl Fixer for UncertainFixer {
fn name(&self) -> String {
self.name.clone()
}
fn path(&self) -> std::path::PathBuf {
std::path::PathBuf::from("/dev/null")
}
fn lintian_tags(&self) -> Vec<String> {
self.lintian_tags.clone()
}
fn run(
&self,
basedir: &std::path::Path,
_package: &str,
_current_version: &Version,
_preferences: &FixerPreferences,
_timeout: Option<chrono::Duration>,
) -> Result<FixerResult, FixerError> {
std::fs::write(basedir.join("debian/somefile"), "test").unwrap();
Ok(FixerResult {
description: "Renamed a file.".to_string(),
patch_name: None,
certainty: Some(Certainty::Possible),
fixed_lintian_issues: vec![],
overridden_lintian_issues: vec![],
revision_id: None,
})
}
}
let lock_write = tree.lock_write().unwrap();
let (_result, summary) = run_lintian_fixer(
&tree,
&UncertainFixer::new("dummy", &["some-tag"]),
Some("Testsuite <lintian-brush@example.com>"),
|| false,
&FixerPreferences {
minimum_certainty: Some(Certainty::Possible),
..Default::default()
},
&mut None,
Path::new(""),
None,
None,
None,
None,
)
.unwrap();
assert_eq!("Renamed a file.", summary);
assert_eq!(2, tree.branch().revno());
std::mem::drop(lock_write);
std::mem::drop(td);
}
#[test]
fn test_new_file() {
let (td, tree) = setup(None);
#[derive(Debug)]
struct NewFileFixer {
name: String,
lintian_tags: Vec<String>,
}
impl NewFileFixer {
fn new(name: &str, lintian_tags: &[&str]) -> Self {
Self {
name: name.to_string(),
lintian_tags: lintian_tags.iter().map(|t| t.to_string()).collect(),
}
}
}
impl Fixer for NewFileFixer {
fn name(&self) -> String {
self.name.clone()
}
fn path(&self) -> std::path::PathBuf {
std::path::PathBuf::from("/dev/null")
}
fn lintian_tags(&self) -> Vec<String> {
self.lintian_tags.clone()
}
fn run(
&self,
basedir: &std::path::Path,
package: &str,
_current_version: &Version,
_preferences: &FixerPreferences,
_timeout: Option<chrono::Duration>,
) -> Result<FixerResult, FixerError> {
std::fs::write(basedir.join("debian/somefile"), "test").unwrap();
Ok(FixerResult {
description: "Created new file.".to_string(),
patch_name: None,
certainty: None,
fixed_lintian_issues: vec![LintianIssue {
tag: Some("some-tag".to_string()),
package: Some(package.to_string()),
info: None,
package_type: Some(PackageType::Source),
}],
overridden_lintian_issues: vec![],
revision_id: None,
})
}
}
let lock_write = tree.lock_write().unwrap();
let (result, summary) = run_lintian_fixer(
&tree,
&NewFileFixer::new("new-file", &["some-tag"]),
Some(COMMITTER),
|| false,
&FixerPreferences::default(),
&mut None,
Path::new(""),
None,
None,
None,
None,
)
.unwrap();
assert_eq!("Created new file.", summary);
assert_eq!(result.certainty, None);
assert_eq!(result.fixed_lintian_tags(), &["some-tag"]);
let rev = tree
.branch()
.repository()
.get_revision(&tree.last_revision().unwrap())
.unwrap();
assert_eq!(
rev.message,
"Created new file.\n\nChanges-By: lintian-brush\nFixes: lintian: some-tag\nSee-also: https://lintian.debian.org/tags/some-tag.html\n"
);
assert_eq!(2, tree.branch().revno());
let basis_tree = tree.branch().basis_tree().unwrap();
let basis_lock = basis_tree.lock_read().unwrap();
assert_eq!(
basis_tree
.get_file_text(Path::new("debian/somefile"))
.unwrap(),
b"test"
);
std::mem::drop(basis_lock);
std::mem::drop(lock_write);
std::mem::drop(td);
}
#[test]
fn test_rename_file() {
let (td, tree) = setup(None);
#[derive(Debug)]
struct RenameFileFixer {
name: String,
lintian_tags: Vec<String>,
}
impl RenameFileFixer {
fn new(name: &str, lintian_tags: &[&str]) -> Self {
Self {
name: name.to_string(),
lintian_tags: lintian_tags.iter().map(|t| t.to_string()).collect(),
}
}
}
impl Fixer for RenameFileFixer {
fn name(&self) -> String {
self.name.clone()
}
fn path(&self) -> std::path::PathBuf {
std::path::PathBuf::from("/dev/null")
}
fn lintian_tags(&self) -> Vec<String> {
self.lintian_tags.clone()
}
fn run(
&self,
basedir: &std::path::Path,
_package: &str,
_current_version: &Version,
_preferences: &FixerPreferences,
_timeout: Option<chrono::Duration>,
) -> Result<FixerResult, FixerError> {
std::fs::rename(
basedir.join("debian/control"),
basedir.join("debian/control.blah"),
)
.unwrap();
Ok(FixerResult {
description: "Renamed a file.".to_string(),
patch_name: None,
certainty: None,
fixed_lintian_issues: vec![],
overridden_lintian_issues: vec![],
revision_id: None,
})
}
}
let orig_basis_tree = tree.branch().basis_tree().unwrap();
let lock_write = tree.lock_write().unwrap();
let (result, summary) = run_lintian_fixer(
&tree,
&RenameFileFixer::new("rename", &["some-tag"]),
Some(COMMITTER),
|| false,
&FixerPreferences::default(),
&mut None,
Path::new(""),
None,
None,
None,
None,
)
.unwrap();
assert_eq!("Renamed a file.", summary);
assert_eq!(result.certainty, None);
assert_eq!(2, tree.branch().revno());
let basis_tree = tree.branch().basis_tree().unwrap();
let basis_lock = basis_tree.lock_read().unwrap();
let orig_basis_tree_lock = orig_basis_tree.lock_read().unwrap();
assert!(!basis_tree.has_filename(Path::new("debian/control")));
assert!(basis_tree.has_filename(Path::new("debian/control.blah")));
assert_ne!(
orig_basis_tree.get_revision_id(),
basis_tree.get_revision_id()
);
std::mem::drop(orig_basis_tree_lock);
std::mem::drop(basis_lock);
std::mem::drop(lock_write);
std::mem::drop(td);
}
#[test]
fn test_empty_change() {
let (td, tree) = setup(None);
#[derive(Debug)]
struct EmptyFixer {
name: String,
lintian_tags: Vec<String>,
}
impl EmptyFixer {
fn new(name: &str, lintian_tags: &[&str]) -> Self {
Self {
name: name.to_string(),
lintian_tags: lintian_tags.iter().map(|t| t.to_string()).collect(),
}
}
}
impl Fixer for EmptyFixer {
fn name(&self) -> String {
self.name.clone()
}
fn path(&self) -> std::path::PathBuf {
std::path::PathBuf::from("/dev/null")
}
fn lintian_tags(&self) -> Vec<String> {
self.lintian_tags.clone()
}
fn run(
&self,
_basedir: &std::path::Path,
_package: &str,
_current_version: &Version,
_preferences: &FixerPreferences,
_timeout: Option<chrono::Duration>,
) -> Result<FixerResult, FixerError> {
Ok(FixerResult {
description: "I didn't actually change anything.".to_string(),
patch_name: None,
certainty: None,
fixed_lintian_issues: vec![],
overridden_lintian_issues: vec![],
revision_id: None,
})
}
}
let lock_write = tree.lock_write().unwrap();
let result = run_lintian_fixer(
&tree,
&EmptyFixer::new("empty", &["some-tag"]),
Some(COMMITTER),
|| false,
&FixerPreferences::default(),
&mut None,
Path::new(""),
None,
None,
None,
None,
);
assert!(matches!(result, Err(FixerError::NoChanges)), "{:?}", result);
assert_eq!(1, tree.branch().revno());
assert_eq!(
Vec::<breezyshim::tree::TreeChange>::new(),
tree.iter_changes(&tree.basis_tree().unwrap(), None, None, None)
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap()
);
std::mem::drop(lock_write);
std::mem::drop(td);
}
#[test]
fn test_upstream_change() {
let (td, tree) = setup(Some("0.1-1"));
#[derive(Debug)]
struct NewFileFixer {
name: String,
lintian_tags: Vec<String>,
}
impl NewFileFixer {
fn new(name: &str, lintian_tags: &[&str]) -> Self {
Self {
name: name.to_string(),
lintian_tags: lintian_tags.iter().map(|t| t.to_string()).collect(),
}
}
}
impl Fixer for NewFileFixer {
fn name(&self) -> String {
self.name.clone()
}
fn path(&self) -> std::path::PathBuf {
std::path::PathBuf::from("/dev/null")
}
fn lintian_tags(&self) -> Vec<String> {
self.lintian_tags.clone()
}
fn run(
&self,
basedir: &std::path::Path,
_package: &str,
_current_version: &Version,
_preferences: &FixerPreferences,
_timeout: Option<chrono::Duration>,
) -> Result<FixerResult, FixerError> {
std::fs::write(basedir.join("configure.ac"), "AC_INIT(foo, bar)\n").unwrap();
Ok(FixerResult {
description: "Created new configure.ac.".to_string(),
patch_name: Some("add-config".to_string()),
certainty: None,
fixed_lintian_issues: vec![],
overridden_lintian_issues: vec![],
revision_id: None,
})
}
}
let lock = tree.lock_write().unwrap();
let (result, summary) = run_lintian_fixer(
&tree,
&NewFileFixer::new("add-config", &["add-config"]),
Some(COMMITTER),
|| false,
&FixerPreferences::default(),
&mut None,
Path::new(""),
Some(
chrono::DateTime::parse_from_rfc3339("2020-09-08T00:36:35Z")
.unwrap()
.naive_utc(),
),
None,
None,
None,
)
.unwrap();
assert_eq!(
summary,
"Add patch add-config.patch: Created new configure.ac."
);
assert_eq!(result.certainty, None);
let rev = tree
.branch()
.repository()
.get_revision(&tree.last_revision().unwrap())
.unwrap();
assert_eq!(
rev.message,
"Created new configure.ac.\n\nChanges-By: lintian-brush\n"
);
assert_eq!(2, tree.branch().revno());
let basis_tree = tree.branch().basis_tree().unwrap();
let basis_lock = basis_tree.lock_read().unwrap();
assert_eq!(
basis_tree
.get_file_text(Path::new("debian/patches/series"))
.unwrap(),
b"add-config.patch\n"
);
let lines = basis_tree
.get_file_lines(Path::new("debian/patches/add-config.patch"))
.unwrap();
assert_eq!(lines[0], b"Description: Created new configure.ac.\n");
assert_eq!(lines[1], b"Origin: other\n");
assert_eq!(lines[2], b"Last-Update: 2020-09-08\n");
assert_eq!(lines[3], b"---\n");
assert_eq!(lines[4], b"=== added file 'configure.ac'\n");
assert_eq!(
&lines[5][..(b"--- a/configure.ac".len())],
b"--- a/configure.ac"
);
assert_eq!(
&lines[6][..(b"+++ b/configure.ac".len())],
b"+++ b/configure.ac"
);
assert_eq!(lines[7], b"@@ -0,0 +1,1 @@\n");
assert_eq!(lines[8], b"+AC_INIT(foo, bar)\n");
std::mem::drop(basis_lock);
std::mem::drop(lock);
std::mem::drop(td);
}
#[test]
fn test_upstream_change_stacked() {
let (td, tree) = setup(Some("0.1-1"));
std::fs::create_dir(td.path().join("debian/patches")).unwrap();
std::fs::write(td.path().join("debian/patches/series"), "foo\n").unwrap();
std::fs::write(
td.path().join("debian/patches/foo"),
r###"--- /dev/null 2020-09-07 13:26:27.546468905 +0000
+++ a 2020-09-08 01:26:25.811742671 +0000
@@ -0,0 +1 @@
+foo
"###,
)
.unwrap();
tree.add(&[
Path::new("debian/patches"),
Path::new("debian/patches/series"),
Path::new("debian/patches/foo"),
])
.unwrap();
tree.build_commit()
.committer(COMMITTER)
.message("Add patches").commit().unwrap();
#[derive(Debug)]
struct NewFileFixer {
name: String,
lintian_tags: Vec<String>,
}
impl NewFileFixer {
fn new(name: &str, lintian_tags: &[&str]) -> Self {
Self {
name: name.to_string(),
lintian_tags: lintian_tags.iter().map(|t| t.to_string()).collect(),
}
}
}
impl Fixer for NewFileFixer {
fn name(&self) -> String {
self.name.clone()
}
fn path(&self) -> std::path::PathBuf {
std::path::PathBuf::from("/dev/null")
}
fn lintian_tags(&self) -> Vec<String> {
self.lintian_tags.clone()
}
fn run(
&self,
basedir: &std::path::Path,
_package: &str,
_current_version: &Version,
_preferences: &FixerPreferences,
_timeout: Option<chrono::Duration>,
) -> Result<FixerResult, FixerError> {
std::fs::write(basedir.join("configure.ac"), "AC_INIT(foo, bar)\n").unwrap();
Ok(FixerResult {
description: "Created new configure.ac.".to_string(),
patch_name: Some("add-config".to_string()),
certainty: None,
fixed_lintian_issues: vec![],
overridden_lintian_issues: vec![],
revision_id: None,
})
}
}
let lock = tree.lock_write().unwrap();
let result = run_lintian_fixer(
&tree,
&NewFileFixer::new("add-config", &["add-config"]),
Some(COMMITTER),
|| false,
&FixerPreferences::default(),
&mut None,
Path::new(""),
Some(
chrono::DateTime::parse_from_rfc3339("2020-09-08T00:36:35Z")
.unwrap()
.naive_utc(),
),
None,
None,
None,
);
std::mem::drop(lock);
assert!(matches!(
result,
Err(FixerError::FailedPatchManipulation(..))
));
std::mem::drop(td);
}
fn make_package_tree(path: &Path, format: &str) -> WorkingTree {
let tree = create_standalone_workingtree(path, format).unwrap();
std::fs::create_dir(path.join("debian")).unwrap();
std::fs::write(
path.join("debian/control"),
r#""Source: blah
Vcs-Git: https://example.com/blah
Testsuite: autopkgtest
Binary: blah
Arch: all
"#,
)
.unwrap();
std::fs::write(
path.join("debian/changelog"),
r#"blah (0.1-1) UNRELEASED; urgency=medium
* Initial release. (Closes: #911016)
-- Blah <example@debian.org> Sat, 13 Oct 2018 11:21:39 +0100
"#,
)
.unwrap();
tree.add(&[
Path::new("debian"),
Path::new("debian/changelog"),
Path::new("debian/control"),
])
.unwrap();
tree.build_commit()
.committer(COMMITTER)
.message("Initial thingy.")
.commit()
.unwrap();
tree
}
fn make_change(tree: &WorkingTree, committer: Option<&str>) {
let lock = tree.lock_write().unwrap();
let (result, summary) = run_lintian_fixer(
tree,
&DummyFixer::new("dummy", &["some-tag"]),
committer,
|| false,
&FixerPreferences::default(),
&mut None,
Path::new(""),
None,
None,
None,
None,
)
.unwrap();
assert_eq!(summary, "Fixed some tag.");
assert_eq!(vec!["some-tag"], result.fixed_lintian_tags());
assert_eq!(Some(Certainty::Certain), result.certainty);
assert_eq!(2, tree.branch().revno());
let lines = tree.get_file_lines(Path::new("debian/control")).unwrap();
assert_eq!(lines.last().unwrap(), b"a new line\n");
std::mem::drop(lock);
}
#[test]
fn test_honors_tree_committer_specified() {
let td = tempfile::tempdir().unwrap();
let tree = make_package_tree(td.path(), "git");
make_change(&tree, Some("Jane Example <jane@example.com>"));
let rev = tree
.branch()
.repository()
.get_revision(&tree.branch().last_revision())
.unwrap();
assert_eq!(rev.committer, "Jane Example <jane@example.com>");
}
#[test]
fn test_honors_tree_committer_config() {
let td = tempfile::tempdir().unwrap();
let tree = make_package_tree(td.path(), "git");
std::fs::write(
td.path().join(".git/config"),
r###"
[user]
email = jane@example.com
name = Jane Example
"###,
)
.unwrap();
make_change(&tree, None);
let rev = tree
.branch()
.repository()
.get_revision(&tree.branch().last_revision())
.unwrap();
assert_eq!(rev.committer, "Jane Example <jane@example.com>");
}
}
#[test]
fn test_find_shell_scripts() {
let td = tempfile::tempdir().unwrap();
let fixers = td.path().join("fixers");
std::fs::create_dir(&fixers).unwrap();
std::fs::create_dir(fixers.join("anotherdir")).unwrap();
std::fs::write(fixers.join("foo.sh"), "echo 'hello'").unwrap();
std::fs::write(fixers.join("bar.sh"), "echo 'hello'").unwrap();
std::fs::write(fixers.join("i-fix-aanother-tag.py"), "print('hello')").unwrap();
std::fs::write(fixers.join(".hidden"), "echo 'hello'").unwrap();
std::fs::write(fixers.join("backup-file.sh~"), "echo 'hello'").unwrap();
std::fs::write(fixers.join("no-extension"), "echo 'hello'").unwrap();
std::fs::write(
fixers.join("index.desc"),
r###"
- script: foo.sh
lintian-tags:
- i-fix-a-tag
- script: bar.sh
lintian-tags:
- i-fix-another-tag
- no-extension
"###,
)
.unwrap();
let fixers = available_lintian_fixers(Some(&fixers), Some(false))
.unwrap()
.collect::<Vec<_>>();
assert_eq!(2, fixers.len());
assert_eq!(fixers[0].name(), "foo");
assert_eq!(fixers[1].name(), "bar");
}
mod many_result_tests {
use super::*;
#[test]
fn test_empty() {
let result = ManyResult::default();
assert_eq!(Certainty::Certain, result.minimum_success_certainty());
}
#[test]
fn test_no_certainty() {
let mut result = ManyResult::default();
result.success.push((
FixerResult::new(
"Do bla".to_string(),
Some(vec!["tag-a".to_string()]),
None,
None,
None,
vec![],
None,
),
"summary".to_string(),
));
assert_eq!(Certainty::Certain, result.minimum_success_certainty());
}
#[test]
fn test_possible() {
let mut result = ManyResult::default();
result.success.push((
FixerResult::new(
"Do bla".to_string(),
Some(vec!["tag-a".to_string()]),
Some(Certainty::Possible),
None,
None,
vec![],
None,
),
"summary".to_string(),
));
result.success.push((
FixerResult::new(
"Do bloeh".to_string(),
Some(vec!["tag-b".to_string()]),
Some(Certainty::Certain),
None,
None,
vec![],
None,
),
"summary".to_string(),
));
assert_eq!(Certainty::Possible, result.minimum_success_certainty());
}
}
}
#[cfg(test)]
mod fixer_tests;