diff --git a/.gitignore b/.gitignore index 28bd600e4..b14aa98dc 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ flamegraph.svg /.markdownlint.json /.rustfmt.toml /.yamllint.yaml -tmp/ \ No newline at end of file +tmp/ + +docs/developer/plans \ No newline at end of file diff --git a/qlty-cli/src/commands/coverage/publish.rs b/qlty-cli/src/commands/coverage/publish.rs index 0c8e1d58a..df31dc431 100644 --- a/qlty-cli/src/commands/coverage/publish.rs +++ b/qlty-cli/src/commands/coverage/publish.rs @@ -433,6 +433,14 @@ impl Publish { } self.print_section_header(" COVERAGE DATA "); + + if let Some(ref git_path) = report.git_repo_path { + eprintln!(" Git Repository: {}", style(git_path).dim()); + } else { + eprintln!(" Git Repository: {}", style("not found").dim()); + } + eprintln!(); + if report.auto_path_fixing_enabled { eprintln!(" Auto-path fixing: Enabled"); } @@ -497,7 +505,7 @@ impl Publish { let (paths_to_show, show_all) = if self.verbose { (missing_files.len(), true) } else { - (std::cmp::min(20, missing_files.len()), false) + (std::cmp::min(50, missing_files.len()), false) }; eprintln!("\n {}\n", style("Missing code files:").bold().yellow()); @@ -552,6 +560,62 @@ impl Publish { ); } + if !report.untracked_files.is_empty() && report.git_repo_path.is_some() { + let mut untracked = report.untracked_files.iter().collect::>(); + untracked.sort(); + + eprintln!( + " {} {} on disk but {} in Git", + style(untracked.len().to_formatted_string(&Locale::en)).red(), + if untracked.len() == 1 { + "path exists" + } else { + "paths exist" + }, + style("not tracked").red() + ); + + let (paths_to_show, show_all) = if self.verbose { + (untracked.len(), true) + } else { + (std::cmp::min(50, untracked.len()), false) + }; + + eprintln!( + "\n {}\n", + style("Untracked code files (not in Git):").bold().red() + ); + + for path in untracked.iter().take(paths_to_show) { + eprintln!( + " {} {}", + style("*").red(), + style(path.to_string()).red() + ); + } + + if !show_all && paths_to_show < untracked.len() { + let remaining = untracked.len() - paths_to_show; + eprintln!( + " {} {}", + style(format!( + "... and {} more", + remaining.to_formatted_string(&Locale::en) + )) + .dim() + .red(), + style("(Use --verbose to see all)").dim() + ); + } + + eprintln!(); + eprintln!( + " {} Files not tracked in Git may not appear in coverage reports.", + style("NOTE:").bold() + ); + eprintln!(); + } + eprintln!(); // Get formatted numbers first diff --git a/qlty-cli/tests/cmd/coverage/basic.stderr b/qlty-cli/tests/cmd/coverage/basic.stderr index 7c167de79..baa95a8f7 100644 --- a/qlty-cli/tests/cmd/coverage/basic.stderr +++ b/qlty-cli/tests/cmd/coverage/basic.stderr @@ -26,6 +26,8 @@ https://qlty.sh/d/coverage COVERAGE DATA + Git Repository: [CWD]/ + Auto-path fixing: Enabled 1 unique code file path All code files in the coverage data were found on disk (count: 1). diff --git a/qlty-cli/tests/cmd/coverage/discover_java_src_dirs.stderr b/qlty-cli/tests/cmd/coverage/discover_java_src_dirs.stderr index 27ba15ec3..4dc1a4fd0 100644 --- a/qlty-cli/tests/cmd/coverage/discover_java_src_dirs.stderr +++ b/qlty-cli/tests/cmd/coverage/discover_java_src_dirs.stderr @@ -31,6 +31,8 @@ https://qlty.sh/d/coverage COVERAGE DATA + Git Repository: [CWD]/ + Auto-path fixing: Enabled 1 unique code file path All code files in the coverage data were found on disk (count: 1). diff --git a/qlty-cli/tests/cmd/coverage/files_exist.stderr b/qlty-cli/tests/cmd/coverage/files_exist.stderr index 41cdfd880..2b7d40214 100644 --- a/qlty-cli/tests/cmd/coverage/files_exist.stderr +++ b/qlty-cli/tests/cmd/coverage/files_exist.stderr @@ -31,6 +31,8 @@ https://qlty.sh/d/coverage COVERAGE DATA + Git Repository: [CWD]/ + Auto-path fixing: Enabled WARNING: 1 code file path was excluded by configuration rules diff --git a/qlty-cli/tests/cmd/coverage/ignore_patterns.stderr b/qlty-cli/tests/cmd/coverage/ignore_patterns.stderr index 43aedb051..b50aef304 100644 --- a/qlty-cli/tests/cmd/coverage/ignore_patterns.stderr +++ b/qlty-cli/tests/cmd/coverage/ignore_patterns.stderr @@ -26,6 +26,8 @@ https://qlty.sh/d/coverage COVERAGE DATA + Git Repository: [CWD]/ + Auto-path fixing: Enabled 2 unique code file paths 2 paths are missing on disk (100.0%) diff --git a/qlty-cli/tests/cmd/coverage/json.stderr b/qlty-cli/tests/cmd/coverage/json.stderr index b31fd3fc2..10f88760a 100644 --- a/qlty-cli/tests/cmd/coverage/json.stderr +++ b/qlty-cli/tests/cmd/coverage/json.stderr @@ -26,6 +26,8 @@ https://qlty.sh/d/coverage COVERAGE DATA + Git Repository: [CWD]/ + 1 unique code file path 1 path is missing on disk (100.0%) diff --git a/qlty-cli/tests/cmd/coverage/override_commit_time.stderr b/qlty-cli/tests/cmd/coverage/override_commit_time.stderr index 930fec459..2abba5594 100644 --- a/qlty-cli/tests/cmd/coverage/override_commit_time.stderr +++ b/qlty-cli/tests/cmd/coverage/override_commit_time.stderr @@ -27,6 +27,8 @@ https://qlty.sh/d/coverage COVERAGE DATA + Git Repository: [CWD]/ + Auto-path fixing: Enabled 1 unique code file path 1 path is missing on disk (100.0%) diff --git a/qlty-cli/tests/cmd/coverage/override_git_tag.stderr b/qlty-cli/tests/cmd/coverage/override_git_tag.stderr index 516bfdec1..bafd87893 100644 --- a/qlty-cli/tests/cmd/coverage/override_git_tag.stderr +++ b/qlty-cli/tests/cmd/coverage/override_git_tag.stderr @@ -27,6 +27,8 @@ https://qlty.sh/d/coverage COVERAGE DATA + Git Repository: /Users/bhelmkamp/p/qltysh/qlty/ + Auto-path fixing: Enabled 1 unique code file path 1 path is missing on disk (100.0%) diff --git a/qlty-cli/tests/cmd/coverage/overrides.stderr b/qlty-cli/tests/cmd/coverage/overrides.stderr index 0d6e41d44..4a6bffa33 100644 --- a/qlty-cli/tests/cmd/coverage/overrides.stderr +++ b/qlty-cli/tests/cmd/coverage/overrides.stderr @@ -31,6 +31,8 @@ WARNING: --transform-add-prefix is deprecated, use --add-prefix instead COVERAGE DATA + Git Repository: [CWD]/ + 1 unique code file path 1 path is missing on disk (100.0%) diff --git a/qlty-cli/tests/cmd/coverage/publish_validate.stderr b/qlty-cli/tests/cmd/coverage/publish_validate.stderr index 5d76f543c..a563ef626 100644 --- a/qlty-cli/tests/cmd/coverage/publish_validate.stderr +++ b/qlty-cli/tests/cmd/coverage/publish_validate.stderr @@ -26,6 +26,8 @@ https://qlty.sh/d/coverage COVERAGE DATA + Git Repository: [CWD]/ + Auto-path fixing: Enabled 2 unique code file paths 1 path is missing on disk (50.0%) diff --git a/qlty-cli/tests/cmd/without_git/basic_coverage.stderr b/qlty-cli/tests/cmd/without_git/basic_coverage.stderr index 7f632dabb..76fe0fe4e 100644 --- a/qlty-cli/tests/cmd/without_git/basic_coverage.stderr +++ b/qlty-cli/tests/cmd/without_git/basic_coverage.stderr @@ -28,6 +28,8 @@ https://qlty.sh/d/coverage COVERAGE DATA + Git Repository: not found + 1 unique code file path 1 path is missing on disk (100.0%) diff --git a/qlty-coverage/src/git.rs b/qlty-coverage/src/git.rs index 7e71c7cc8..4fd53e6ff 100644 --- a/qlty-coverage/src/git.rs +++ b/qlty-coverage/src/git.rs @@ -1,6 +1,7 @@ use anyhow::{Context as _, Result}; use git2::Repository; -use tracing::{error, warn}; +use std::collections::HashSet; +use tracing::{error, info, warn}; #[derive(Debug, Clone)] pub struct CommitMetadata { @@ -68,3 +69,98 @@ pub fn retrieve_commit_metadata() -> Result> { commit_message, })) } + +#[derive(Debug, Clone)] +pub struct GitTrackingInfo { + pub repo_root: String, + pub tracked_files: HashSet, +} + +impl GitTrackingInfo { + pub fn is_tracked(&self, relative_path: &str) -> bool { + let normalized = relative_path.replace('\\', "/"); + self.tracked_files.contains(&normalized) + } +} + +pub fn get_git_tracking_info() -> Option { + if std::env::var("QLTY_COVERAGE_TESTING_WITHOUT_GIT").is_ok() { + return None; + } + + let repo = match Repository::discover(".") { + Ok(repo) => repo, + Err(_) => { + info!("No Git repository found"); + return None; + } + }; + + let repo_root = match repo.workdir() { + Some(path) => match path.to_str() { + Some(s) => s.to_string(), + None => { + warn!("Git repository path is not valid UTF-8"); + return None; + } + }, + None => { + warn!("Git repository has no working directory (bare repository)"); + return None; + } + }; + + let index = match repo.index() { + Ok(index) => index, + Err(err) => { + warn!("Failed to read Git index: {:?}", err); + return None; + } + }; + + let mut tracked_files = HashSet::new(); + for entry in index.iter() { + if let Ok(path) = std::str::from_utf8(&entry.path) { + tracked_files.insert(path.to_string()); + } + } + + info!("Git repository found at: {}", repo_root); + + Some(GitTrackingInfo { + repo_root, + tracked_files, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_tracked_returns_false_for_untracked() { + let info = GitTrackingInfo { + repo_root: "/tmp/test".to_string(), + tracked_files: HashSet::from(["src/main.rs".to_string()]), + }; + assert!(!info.is_tracked("src/untracked.rs")); + } + + #[test] + fn test_is_tracked_returns_true_for_tracked() { + let info = GitTrackingInfo { + repo_root: "/tmp/test".to_string(), + tracked_files: HashSet::from(["src/main.rs".to_string()]), + }; + assert!(info.is_tracked("src/main.rs")); + } + + #[test] + fn test_is_tracked_normalizes_windows_paths() { + let info = GitTrackingInfo { + repo_root: "/tmp/test".to_string(), + tracked_files: HashSet::from(["src/main.rs".to_string()]), + }; + assert!(info.is_tracked("src\\main.rs")); + } +} diff --git a/qlty-coverage/src/publish/processor.rs b/qlty-coverage/src/publish/processor.rs index 1e75b84e4..183da2c65 100644 --- a/qlty-coverage/src/publish/processor.rs +++ b/qlty-coverage/src/publish/processor.rs @@ -1,3 +1,4 @@ +use crate::git::get_git_tracking_info; use crate::publish::{metrics::CoverageMetrics, Plan, Report, Results}; use anyhow::Result; use qlty_types::tests::v1::FileCoverage; @@ -34,14 +35,23 @@ impl Processor { .filter_map(|file_coverage| self.transform(file_coverage.to_owned())) .collect::>(); + let git_tracking = get_git_tracking_info(); + let git_repo_path = git_tracking.as_ref().map(|info| info.repo_root.clone()); + let mut found_files = HashSet::new(); let mut missing_files = HashSet::new(); + let mut untracked_files = HashSet::new(); if self.plan.skip_missing_files { transformed_file_coverages.retain(|file_coverage| { match PathBuf::from(&file_coverage.path).try_exists() { Ok(true) => { found_files.insert(file_coverage.path.clone()); + if let Some(ref tracking) = git_tracking { + if !tracking.is_tracked(&file_coverage.path) { + untracked_files.insert(file_coverage.path.clone()); + } + } true } _ => { @@ -55,6 +65,11 @@ impl Processor { match PathBuf::from(&file_coverage.path).try_exists() { Ok(true) => { found_files.insert(file_coverage.path.clone()); + if let Some(ref tracking) = git_tracking { + if !tracking.is_tracked(&file_coverage.path) { + untracked_files.insert(file_coverage.path.clone()); + } + } } _ => { missing_files.insert(file_coverage.path.clone()); @@ -74,6 +89,8 @@ impl Processor { totals, missing_files, found_files, + untracked_files, + git_repo_path, excluded_files_count: ignored_paths_count, auto_path_fixing_enabled: self.plan.auto_path_fixing_enabled, }) diff --git a/qlty-coverage/src/publish/report.rs b/qlty-coverage/src/publish/report.rs index 90f2d1664..fc5ab0a93 100644 --- a/qlty-coverage/src/publish/report.rs +++ b/qlty-coverage/src/publish/report.rs @@ -18,6 +18,12 @@ pub struct Report { #[serde(skip_serializing)] pub missing_files: HashSet, + #[serde(skip_serializing)] + pub untracked_files: HashSet, + + #[serde(skip_serializing)] + pub git_repo_path: Option, + pub totals: CoverageMetrics, pub excluded_files_count: usize, diff --git a/qlty-coverage/src/validate.rs b/qlty-coverage/src/validate.rs index e3b2cc4d4..2863e25b4 100644 --- a/qlty-coverage/src/validate.rs +++ b/qlty-coverage/src/validate.rs @@ -94,15 +94,15 @@ mod tests { use std::fs::{self, File}; use tempfile::tempdir; - // Helper function to create a test Report instance fn create_test_report(file_coverages: Vec) -> Report { - // Create a minimal valid Report Report { metadata: CoverageMetadata::default(), report_files: vec![ReportFile::default()], file_coverages, found_files: HashSet::new(), missing_files: HashSet::new(), + untracked_files: HashSet::new(), + git_repo_path: None, totals: Default::default(), excluded_files_count: 0, auto_path_fixing_enabled: false,