//! Pkh-native checks: workflow knowledge lintian cannot have, because it //! lives in pkh's flows (git-centric trees, PPA uploads, scaffolding). use crate::lint::check::Check; use crate::lint::collect::LintData; use crate::lint::emit::Emitter; use crate::lint::tag::{Certainty, Severity, Tag}; /// Metadata of every tag the pkh-native checks emit. pub static TAGS: &[Tag] = &[Tag { name: "pkh-debian-changes-not-committed", severity: Severity::Warning, certainty: Certainty::Certain, experimental: false, description: "The debian/ directory contains changes that are not committed to git. \ pkh builds and uploads the tree as-is (pkh deb, pkh put); committing first keeps \ the upload and the git history in sync.", references: &[], }]; /// Flags `debian/` content that exists in the tree but is not committed to /// git. Skips trees outside any git repository: archive-pulled sources /// legitimately have none. pub struct PkhGit; impl Check for PkhGit { fn id(&self) -> &'static str { "pkh-git" } fn tags(&self) -> &'static [Tag] { TAGS } fn run(&self, data: &LintData, emit: &mut Emitter) { let Some(git) = &data.git else { return; }; if git.dirty_debian.is_empty() { return; } let examples: Vec<&str> = git .dirty_debian .iter() .take(3) .map(String::as_str) .collect(); let more = if git.dirty_debian.len() > examples.len() { ", ..." } else { "" }; emit.tag( "pkh-debian-changes-not-committed", format!( "{} uncommitted change(s) under debian/ (e.g. {}{more})", git.dirty_debian.len(), examples.join(", ") ), ); } } #[cfg(test)] mod tests { use super::*; use std::path::Path; fn write(path: &Path, content: &str) { std::fs::create_dir_all(path.parent().unwrap()).unwrap(); std::fs::write(path, content).unwrap(); } fn commit_all(repo: &git2::Repository) { let signature = git2::Signature::now("pkh test", "test@example.com").unwrap(); let mut index = repo.index().unwrap(); index .add_all(["*"], git2::IndexAddOption::DEFAULT, None) .unwrap(); // write_tree alone does not persist the index; without this, a // committed worktree still reads as index-deleted + untracked. index.write().unwrap(); let tree_id = index.write_tree().unwrap(); let tree = repo.find_tree(tree_id).unwrap(); repo.commit(Some("HEAD"), &signature, &signature, "init", &tree, &[]) .unwrap(); } #[test] fn no_git_repository_is_no_finding() { let dir = tempfile::tempdir().unwrap(); write(&dir.path().join("debian/control"), "Source: hello\n"); let data = LintData::collect(dir.path(), None, None); assert!(data.git.is_none()); let mut findings = Vec::new(); let mut emitter = Emitter::new(data.source_name.clone(), &mut findings); PkhGit.run(&data, &mut emitter); assert!(findings.is_empty()); } #[test] fn clean_worktree_is_no_finding() { let dir = tempfile::tempdir().unwrap(); write(&dir.path().join("debian/control"), "Source: hello\n"); let repo = git2::Repository::init(dir.path()).unwrap(); commit_all(&repo); let data = LintData::collect(dir.path(), None, None); let mut findings = Vec::new(); let mut emitter = Emitter::new(data.source_name.clone(), &mut findings); PkhGit.run(&data, &mut emitter); assert!(findings.is_empty()); } #[test] fn dirty_debian_tree_is_a_finding() { let dir = tempfile::tempdir().unwrap(); write(&dir.path().join("debian/control"), "Source: hello\n"); write(&dir.path().join("hello.txt"), "upstream\n"); let repo = git2::Repository::init(dir.path()).unwrap(); commit_all(&repo); // A modified tracked file and a fresh untracked patch: both count. write( &dir.path().join("debian/control"), "Source: hello\nDepends: x\n", ); write(&dir.path().join("debian/patches/new.patch"), "...\n"); write(&dir.path().join("hello.txt"), "changed upstream\n"); let data = LintData::collect(dir.path(), None, None); let mut findings = Vec::new(); let mut emitter = Emitter::new(data.source_name.clone(), &mut findings); PkhGit.run(&data, &mut emitter); assert_eq!(findings.len(), 1); let finding = &findings[0]; assert_eq!(finding.tag_name, "pkh-debian-changes-not-committed"); assert_eq!(finding.letter, 'W'); assert!( finding .message .starts_with("2 uncommitted change(s) under debian/") ); assert!(finding.message.contains("debian/control")); assert!(finding.message.contains("debian/patches/new.patch")); } }