use clap::Parser;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::path::PathBuf;
use std::process::Command;
#[derive(Debug, Deserialize, Serialize)]
struct TagEntry {
tag: String,
#[serde(skip_serializing_if = "Option::is_none")]
status: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
difficulty: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
comment: Option<String>,
}
#[derive(Parser, Debug)]
#[command(author, version, about = "Validate and check tag-status.yaml", long_about = None)]
struct Args {
#[arg(long)]
new_tags: bool,
#[arg(long)]
check: bool,
#[cfg(feature = "udd")]
#[arg(long)]
next: bool,
#[cfg(feature = "udd")]
#[arg(long, default_value = "hard")]
exclude: String,
#[arg(long)]
update_readme: bool,
}
fn get_all_lintian_tags() -> Result<HashSet<String>, Box<dyn Error>> {
let output = Command::new("lintian-explain-tags")
.arg("--list-tags")
.output()?;
if !output.status.success() {
return Err("lintian-explain-tags failed".into());
}
let tags = String::from_utf8(output.stdout)?
.lines()
.map(|s| s.to_string())
.collect();
Ok(tags)
}
fn get_supported_tags() -> HashSet<String> {
lintian_brush::builtin_fixers::get_builtin_fixers()
.iter()
.flat_map(|fixer| fixer.lintian_tags())
.map(|s| s.to_string())
.collect()
}
fn validate_implemented_tags(
supported_tags: &HashSet<String>,
per_tag_status: &HashMap<String, TagEntry>,
) -> Result<(), Box<dyn Error>> {
for tag in supported_tags {
let Some(existing) = per_tag_status.get(tag) else {
continue;
};
let Some(status_str) = &existing.status else {
continue;
};
if status_str != "implemented" {
return Err(format!(
"tag {} is marked as {} in tag-status.yaml, but implemented",
tag, status_str
)
.into());
}
}
Ok(())
}
#[cfg(feature = "udd")]
async fn report_next_tags(
exclude_difficulties: &[&str],
per_tag_status: &HashMap<String, TagEntry>,
supported_tags: &HashSet<String>,
) -> Result<(), Box<dyn Error>> {
use sqlx::Row;
let pool = debian_analyzer::udd::connect_udd_mirror().await?;
let query = "SELECT tag, COUNT(DISTINCT package) AS package_count, \
COUNT(*) AS tag_count FROM lintian \
WHERE tag_type NOT IN ('classification') GROUP BY 1 ORDER BY 2 DESC";
let rows = sqlx::query(query).fetch_all(&pool).await?;
for row in rows {
let tag: String = row.get("tag");
let package_count: i64 = row.get("package_count");
let tag_count: i64 = row.get("tag_count");
if supported_tags.contains(&tag) {
continue;
}
let difficulty = per_tag_status
.get(&tag)
.and_then(|entry| entry.difficulty.as_ref())
.map(|s| s.as_str())
.unwrap_or("unknown");
if exclude_difficulties.contains(&difficulty) {
continue;
}
println!("{} {} {}/{}", tag, difficulty, package_count, tag_count);
}
Ok(())
}
fn update_readme(supported_tags: &HashSet<String>) -> Result<(), Box<dyn Error>> {
let readme_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("README.md");
let contents = std::fs::read_to_string(&readme_path)?;
let mut sorted_tags: Vec<_> = supported_tags.iter().collect();
sorted_tags.sort();
let replacement_text: String = sorted_tags
.iter()
.map(|tag| format!("* {}\n", tag))
.collect();
let re = Regex::new(r"(?s)(subset of the issues:\n\n).*?(\n\.\. _writing-fixers:\n)")?;
if !re.is_match(&contents) {
return Err(format!(
"Could not find tag list markers in {}",
readme_path.display()
)
.into());
}
let updated_contents = re.replace(&contents, |caps: ®ex::Captures| {
format!("{}{}{}", &caps[1], replacement_text, &caps[2])
});
std::fs::write(&readme_path, updated_contents.as_ref())?;
Ok(())
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let args = Args::parse();
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let path = PathBuf::from(manifest_dir).join("tag-status.yaml");
let content = std::fs::read_to_string(&path)?;
let tag_status: Vec<TagEntry> = serde_yaml::from_str(&content)?;
let per_tag_status: HashMap<String, TagEntry> = tag_status
.into_iter()
.map(|entry| (entry.tag.clone(), entry))
.collect();
let supported_tags = get_supported_tags();
validate_implemented_tags(&supported_tags, &per_tag_status)?;
if args.new_tags {
let all_tags = get_all_lintian_tags()?;
let mut missing_tags: Vec<_> = all_tags
.iter()
.filter(|tag| !per_tag_status.contains_key(*tag))
.collect();
missing_tags.sort();
for tag in missing_tags {
println!("{}", tag);
}
} else if args.check {
let all_tags = get_all_lintian_tags()?;
let mut missing_tags: Vec<_> = all_tags
.iter()
.filter(|tag| !per_tag_status.contains_key(*tag))
.collect();
missing_tags.sort();
let mut retcode = 0;
for tag in missing_tags {
println!("Missing tag: {}", tag);
retcode = 1;
}
std::process::exit(retcode);
}
#[cfg(feature = "udd")]
if args.next {
let exclude_difficulties: Vec<&str> = args.exclude.split(',').collect();
report_next_tags(&exclude_difficulties, &per_tag_status, &supported_tags).await?;
}
if args.update_readme {
update_readme(&supported_tags)?;
println!("README.md updated successfully");
}
Ok(())
}