From d7332e479eccb488ee809eabf55614cb5d3bf92e Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Sat, 18 Apr 2026 01:43:25 -0700 Subject: [PATCH 01/18] refactor: migrate to git-bot-feedback lib resolves #97 This has 2 main benefits: 1. Improved PR review support. The [git-bot-feedback] lib automatically hides outdated suggestions and reviews. 2. Any further CI platform support can get implemented in the [git-bot-feedback] lib. This way, cpp-linter gets the newly supported CI platforms for "free." [git-bot-feedback]: https://github.com/2bndy5/git-bot-feedback --- Cargo.lock | 32 + clang-installer/src/downloader/mod.rs | 12 +- clang-installer/src/main.rs | 1 - clang-installer/src/progress_bar.rs | 2 +- clang-installer/src/tool.rs | 17 +- clang-installer/src/version.rs | 4 +- cpp-linter/Cargo.toml | 2 + cpp-linter/src/clang_tools/clang_format.rs | 6 +- cpp-linter/src/clang_tools/clang_tidy.rs | 6 +- cpp-linter/src/clang_tools/mod.rs | 94 +- cpp-linter/src/cli/structs.rs | 48 +- cpp-linter/src/common_fs/file_filter.rs | 281 - cpp-linter/src/common_fs/mod.rs | 39 +- cpp-linter/src/error.rs | 66 + cpp-linter/src/git.rs | 288 +- cpp-linter/src/lib.rs | 3 +- cpp-linter/src/rest_api/github/mod.rs | 450 -- .../src/rest_api/github/serde_structs.rs | 107 - .../src/rest_api/github/specific_api.rs | 543 -- cpp-linter/src/rest_api/mod.rs | 662 --- cpp-linter/src/rest_client/mod.rs | 570 ++ cpp-linter/src/run.rs | 48 +- cpp-linter/tests/.hidden/test_asset.txt | 1 - .../tests/comment_test_assets/patch.diff | 108 - .../comment_test_assets/pr_comments_pg1.json | 2 +- .../comment_test_assets/pr_comments_pg2.json | 2 +- .../tests/comment_test_assets/pr_diff.json | 74 + ...bece420dbbb495.json => push_comments.json} | 12 +- .../tests/comment_test_assets/push_diff.json | 113 + cpp-linter/tests/comments.rs | 53 +- ...ff0b690e1903797c303c5fc8d9f3b52f1d3c5.diff | 5208 ----------------- cpp-linter/tests/ignored_paths/.gitmodules | 12 - cpp-linter/tests/paginated_changed_files.rs | 38 +- cpp-linter/tests/reviews.rs | 102 +- .../tests/reviews_test_assets/pr_27.diff | 108 - .../tests/reviews_test_assets/pr_27.json | 38 + 36 files changed, 1234 insertions(+), 7918 deletions(-) delete mode 100644 cpp-linter/src/common_fs/file_filter.rs create mode 100644 cpp-linter/src/error.rs delete mode 100644 cpp-linter/src/rest_api/github/mod.rs delete mode 100644 cpp-linter/src/rest_api/github/serde_structs.rs delete mode 100644 cpp-linter/src/rest_api/github/specific_api.rs delete mode 100644 cpp-linter/src/rest_api/mod.rs create mode 100644 cpp-linter/src/rest_client/mod.rs delete mode 100644 cpp-linter/tests/.hidden/test_asset.txt delete mode 100644 cpp-linter/tests/comment_test_assets/patch.diff create mode 100644 cpp-linter/tests/comment_test_assets/pr_diff.json rename cpp-linter/tests/comment_test_assets/{push_comments_8d68756375e0483c7ac2b4d6bbbece420dbbb495.json => push_comments.json} (97%) create mode 100644 cpp-linter/tests/comment_test_assets/push_diff.json delete mode 100644 cpp-linter/tests/git_status_test_assets/cpp-linter/cpp-linter/950ff0b690e1903797c303c5fc8d9f3b52f1d3c5.diff delete mode 100644 cpp-linter/tests/ignored_paths/.gitmodules delete mode 100644 cpp-linter/tests/reviews_test_assets/pr_27.diff create mode 100644 cpp-linter/tests/reviews_test_assets/pr_27.json diff --git a/Cargo.lock b/Cargo.lock index eb57e9a8..7b3397d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,6 +98,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -365,12 +376,14 @@ name = "cpp-linter" version = "2.0.0-rc15" dependencies = [ "anyhow", + "async-trait", "chrono", "clang-installer", "clap", "colored", "fast-glob", "futures", + "git-bot-feedback", "git2", "log", "mockito", @@ -749,6 +762,25 @@ dependencies = [ "wasip3", ] +[[package]] +name = "git-bot-feedback" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dde52ca9dc5b3d9227c79b910a804036f0599a563767015683d9b3fa6842874" +dependencies = [ + "async-trait", + "chrono", + "fast-glob", + "log", + "regex", + "reqwest", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "url", +] + [[package]] name = "git2" version = "0.20.4" diff --git a/clang-installer/src/downloader/mod.rs b/clang-installer/src/downloader/mod.rs index 9427373f..78de368f 100644 --- a/clang-installer/src/downloader/mod.rs +++ b/clang-installer/src/downloader/mod.rs @@ -60,7 +60,17 @@ async fn download(url: &Url, cache_path: &Path, timeout: u64) -> Result<(), Down } let mut tmp_file = tempfile::NamedTempFile::new()?; let content_len = response.content_length().and_then(NonZero::new); - let mut progress_bar = ProgressBar::new(content_len, "Downloading"); + let mut progress_bar = ProgressBar::new( + content_len, + format!( + "Downloading {}", + cache_path + .file_name() + .map(|p| p.to_string_lossy()) + .unwrap_or_default() + ) + .as_str(), + ); progress_bar.render()?; while let Some(chunk) = response.chunk().await? { let chunk_len = chunk.len() as u64; diff --git a/clang-installer/src/main.rs b/clang-installer/src/main.rs index f0d92445..c32abb8e 100644 --- a/clang-installer/src/main.rs +++ b/clang-installer/src/main.rs @@ -132,7 +132,6 @@ pub struct CliOptions { async fn main() -> Result<()> { logging::initialize_logger(); let options = CliOptions::parse(); - log::debug!("{:?}", options); let tool = options .tool diff --git a/clang-installer/src/progress_bar.rs b/clang-installer/src/progress_bar.rs index 1a0c35b7..852f6d6a 100644 --- a/clang-installer/src/progress_bar.rs +++ b/clang-installer/src/progress_bar.rs @@ -59,7 +59,7 @@ impl ProgressBar { steps: 0, stdout_handle, is_interactive, - prompt: prompt.to_string(), + prompt: prompt.trim().to_string(), } } diff --git a/clang-installer/src/tool.rs b/clang-installer/src/tool.rs index ae93df06..88503a99 100644 --- a/clang-installer/src/tool.rs +++ b/clang-installer/src/tool.rs @@ -44,8 +44,8 @@ pub enum GetClangVersionError { RegexCompile(#[from] regex::Error), /// Failed to parse the version number from the output of `clang-tool --version`. - #[error("Failed to parse the version number from the `--version` output")] - VersionParse, + #[error("Failed to parse the version number from the `--version` output: {0}")] + VersionParse(String), /// Failed to parse the version number from the output of `clang-tool --version` into a [`semver::Version`]. #[error("Failed to parse the version number from the `--version` output: {0}")] @@ -149,12 +149,13 @@ impl ClangTool { .map_err(|e| GetClangVersionError::Command(path.to_path_buf(), e))?; let stdout = String::from_utf8_lossy(&output.stdout); let version_pattern = Regex::new(r"(?i)version[^\d]*([\d.]+)")?; - let captures = version_pattern - .captures(&stdout) - .ok_or(GetClangVersionError::VersionParse)?; - let result = captures.get(1).ok_or(GetClangVersionError::VersionParse)?; - let version = Version::parse(result.as_str())?; - Ok(version) + if let Some(captures) = version_pattern.captures(&stdout) + && let Some(result) = captures.get(1) + { + let version = Version::parse(result.as_str())?; + return Ok(version); + } + Err(GetClangVersionError::VersionParse(stdout.to_string())) } pub fn symlink_bin( diff --git a/clang-installer/src/version.rs b/clang-installer/src/version.rs index 5d2ea3c4..a603853e 100644 --- a/clang-installer/src/version.rs +++ b/clang-installer/src/version.rs @@ -56,7 +56,7 @@ pub enum GetToolError { ExecutablePathNoParent, /// Failed to capture the clang version from `--version` output. - #[error("Failed to capture the clang version from `--version` output: {0}")] + #[error(transparent)] GetClangVersion(#[from] GetClangVersionError), /// Failed to get the clang executable path. @@ -299,7 +299,7 @@ mod tests { // for this test we should use the oldest supported clang version // because that would be most likely to require downloading. let version_req = - VersionReq::parse(option_env!("MIN_CLANG_TOOLS_VERSION").unwrap_or("11")).unwrap(); + VersionReq::parse(option_env!("MIN_CLANG_TOOLS_VERSION").unwrap_or("16")).unwrap(); let downloaded_clang = RequestedVersion::Requirement(version_req.clone()) .eval_tool(&tool, false, Some(&PathBuf::from(tmp_cache_dir.path()))) .await diff --git a/cpp-linter/Cargo.toml b/cpp-linter/Cargo.toml index d684b287..c8363c82 100644 --- a/cpp-linter/Cargo.toml +++ b/cpp-linter/Cargo.toml @@ -15,12 +15,14 @@ license.workspace = true [dependencies] anyhow = { workspace = true } +async-trait = "0.1.89" chrono = "0.4.44" clang-installer = { path = "../clang-installer", version = "0.1.0" } clap = { workspace = true, optional = true } colored = { workspace = true, optional = true } fast-glob = "1.0.1" futures = "0.3.32" +git-bot-feedback = { version = "0.5.2", features = ["file-changes"] } git2 = "0.20.4" log = { workspace = true } quick-xml = { version = "0.39.2", features = ["serialize"] } diff --git a/cpp-linter/src/clang_tools/clang_format.rs b/cpp-linter/src/clang_tools/clang_format.rs index c4bb2c99..a9e0e7a5 100644 --- a/cpp-linter/src/clang_tools/clang_format.rs +++ b/cpp-linter/src/clang_tools/clang_format.rs @@ -68,12 +68,10 @@ pub fn summarize_style(style: &str) -> String { } /// Get a total count of clang-format advice from the given list of [FileObj]s. -pub fn tally_format_advice(files: &[Arc>]) -> Result { +pub fn tally_format_advice(files: &[Arc>]) -> Result { let mut total = 0; for file in files { - let file = file - .lock() - .map_err(|_| anyhow!("Failed to acquire lock on mutex for a source file"))?; + let file = file.lock().map_err(|e| e.to_string())?; if let Some(advice) = &file.format_advice && !advice.replacements.is_empty() { diff --git a/cpp-linter/src/clang_tools/clang_tidy.rs b/cpp-linter/src/clang_tools/clang_tidy.rs index a45abad8..5724f068 100644 --- a/cpp-linter/src/clang_tools/clang_tidy.rs +++ b/cpp-linter/src/clang_tools/clang_tidy.rs @@ -247,12 +247,10 @@ fn parse_tidy_output( } /// Get a total count of clang-tidy advice from the given list of [FileObj]s. -pub fn tally_tidy_advice(files: &[Arc>]) -> Result { +pub fn tally_tidy_advice(files: &[Arc>]) -> Result { let mut total = 0; for file in files { - let file = file - .lock() - .map_err(|_| anyhow!("Failed to acquire lock on mutex for a source file"))?; + let file = file.lock().map_err(|e| e.to_string())?; if let Some(advice) = &file.tidy_advice { for tidy_note in &advice.notes { let file_path = PathBuf::from(&tidy_note.filename); diff --git a/cpp-linter/src/clang_tools/mod.rs b/cpp-linter/src/clang_tools/mod.rs index ff152e3a..0ab217a2 100644 --- a/cpp-linter/src/clang_tools/mod.rs +++ b/cpp-linter/src/clang_tools/mod.rs @@ -11,15 +11,17 @@ use std::{ // non-std crates use anyhow::{Context, Result, anyhow}; use clang_installer::{ClangTool, RequestedVersion}; +use git_bot_feedback::ReviewComment; use git2::{DiffOptions, Patch}; use semver::Version; use tokio::task::JoinSet; // project-specific modules/crates use super::common_fs::FileObj; +use crate::error::SuggestionError; use crate::{ cli::ClangParams, - rest_api::{COMMENT_MARKER, RestApiClient, USER_OUTREACH}, + rest_client::{RestClient, USER_OUTREACH}, }; pub mod clang_format; use clang_format::run_clang_format; @@ -46,7 +48,7 @@ fn analyze_single_file( if clang_params .format_filter .as_ref() - .is_some_and(|f| f.is_source_or_ignored(file.name.as_path())) + .is_some_and(|f| f.is_qualified(file.name.as_path())) || clang_params.format_filter.is_none() { let format_result = run_clang_format(&mut file, &clang_params)?; @@ -65,7 +67,7 @@ fn analyze_single_file( if clang_params .tidy_filter .as_ref() - .is_some_and(|f| f.is_source_or_ignored(file.name.as_path())) + .is_some_and(|f| f.is_qualified(file.name.as_path())) || clang_params.tidy_filter.is_none() { let tidy_result = run_clang_tidy(&mut file, &clang_params)?; @@ -101,7 +103,7 @@ pub async fn capture_clang_tools_output( files: &[Arc>], version: &RequestedVersion, mut clang_params: ClangParams, - rest_api_client: &impl RestApiClient, + rest_api_client: &RestClient, ) -> Result { let mut clang_versions = ClangVersions::default(); // find the executable paths for clang-tidy and/or clang-format and show version @@ -148,11 +150,12 @@ pub async fn capture_clang_tools_output( // This includes any `spawn()` error and any `analyze_single_file()` error. // Any unresolved tasks are aborted and dropped when an error is returned here. let (file_name, logs) = output??; - rest_api_client.start_log_group(format!("Analyzing {}", file_name.to_string_lossy())); + let log_group_name = format!("Analyzing {}", file_name.to_string_lossy()); + rest_api_client.start_log_group(&log_group_name); for (level, msg) in logs { log::log!(level, "{}", msg); } - rest_api_client.end_log_group(); + rest_api_client.end_log_group(&log_group_name); } Ok(clang_versions) } @@ -169,6 +172,17 @@ pub struct Suggestion { pub path: String, } +impl Suggestion { + pub(crate) fn as_review_comment(&self) -> ReviewComment { + ReviewComment { + line_start: Some(self.line_start), + line_end: self.line_end, + comment: self.suggestion.clone(), + path: self.path.clone(), + } + } +} + /// A struct to describe the Pull Request review suggestions. #[derive(Default)] pub struct ReviewComments { @@ -189,8 +203,12 @@ pub struct ReviewComments { } impl ReviewComments { - pub fn summarize(&self, clang_versions: &ClangVersions) -> String { - let mut body = format!("{COMMENT_MARKER}## Cpp-linter Review\n"); + pub fn summarize( + &self, + clang_versions: &ClangVersions, + comments: &Vec, + ) -> String { + let mut body = String::from("## Cpp-linter Review\n"); for t in 0_usize..=1 { let mut total = 0; let (tool_name, tool_version) = if t == 0 { @@ -209,9 +227,9 @@ impl ReviewComments { if let Some(ver_str) = tool_version { body.push_str(format!("\n### Used {tool_name} v{ver_str}\n").as_str()); } - for comment in &self.comments { + for comment in comments { if comment - .suggestion + .comment .contains(format!("### {tool_name}").as_str()) { total += 1; @@ -266,24 +284,17 @@ pub fn make_patch<'buffer>( path: &Path, patched: &'buffer [u8], original_content: &'buffer [u8], -) -> Result> { +) -> Result, git2::Error> { let mut diff_opts = &mut DiffOptions::new(); diff_opts = diff_opts.indent_heuristic(true); diff_opts = diff_opts.context_lines(0); - let patch = Patch::from_buffers( + Patch::from_buffers( original_content, Some(path), patched, Some(path), Some(diff_opts), ) - .with_context(|| { - format!( - "Failed to create patch for file {}.", - path.to_string_lossy() - ) - })?; - Ok(patch) } /// A trait for generating suggestions from a [`FileObj`]'s advice's generated `patched` buffer. @@ -301,7 +312,7 @@ pub trait MakeSuggestions { file_obj: &FileObj, patch: &mut Patch, summary_only: bool, - ) -> Result<()> { + ) -> Result<(), SuggestionError> { let is_tidy_tool = (&self.get_tool_name() == "clang-tidy") as usize; let hunks_total = patch.num_hunks(); let mut hunks_in_patch = 0u32; @@ -313,11 +324,17 @@ pub trait MakeSuggestions { .to_owned(); let patch_buf = &patch .to_buf() - .with_context(|| "Failed to convert patch to byte array")? + .map_err(|e| SuggestionError::PatchIntoBytesFailed { + file_name: file_name.clone(), + source: e, + })? .to_vec(); review_comments.full_patch[is_tidy_tool].push_str( String::from_utf8(patch_buf.to_owned()) - .with_context(|| format!("Failed to convert patch to string: {file_name}"))? + .map_err(|e| SuggestionError::PatchIntoStringFailed { + file_name: file_name.clone(), + source: e, + })? .as_str(), ); if summary_only { @@ -325,9 +342,14 @@ pub trait MakeSuggestions { return Ok(()); } for hunk_id in 0..hunks_total { - let (hunk, line_count) = patch.hunk(hunk_id).with_context(|| { - format!("Failed to get hunk {hunk_id} from patch for {file_name}") - })?; + let (hunk, line_count) = + patch + .hunk(hunk_id) + .map_err(|e| SuggestionError::GetHunkFailed { + hunk_id, + file_name: file_name.clone(), + source: e, + })?; hunks_in_patch += 1; let hunk_range = file_obj.is_hunk_in_diff(&hunk); match hunk_range { @@ -337,11 +359,23 @@ pub trait MakeSuggestions { let suggestion_help = self.get_suggestion_help(start_line, end_line); let mut removed = vec![]; for line_index in 0..line_count { - let diff_line = patch - .line_in_hunk(hunk_id, line_index) - .with_context(|| format!("Failed to get line {line_index} in a hunk {hunk_id} of patch for {file_name}"))?; - let line = String::from_utf8(diff_line.content().to_owned()) - .with_context(|| format!("Failed to convert line {line_index} buffer to string in hunk {hunk_id} of patch for {file_name}"))?; + let diff_line = patch.line_in_hunk(hunk_id, line_index).map_err(|e| { + SuggestionError::GetHunkLineFailed { + line_index, + hunk_id, + file_name: file_name.clone(), + source: e, + } + })?; + let line = + String::from_utf8(diff_line.content().to_owned()).map_err(|e| { + SuggestionError::HunkLineIntoStringFailed { + line_index, + hunk_id, + file_name: file_name.clone(), + source: e, + } + })?; if ['+', ' '].contains(&diff_line.origin()) { suggestion.push_str(line.as_str()); } else { diff --git a/cpp-linter/src/cli/structs.rs b/cpp-linter/src/cli/structs.rs index 115a1c59..b51605db 100644 --- a/cpp-linter/src/cli/structs.rs +++ b/cpp-linter/src/cli/structs.rs @@ -5,7 +5,9 @@ use clap::{ValueEnum, builder::PossibleValue}; #[cfg(feature = "bin")] use super::Cli; -use crate::{clang_tools::clang_tidy::CompilationUnit, common_fs::FileFilter}; +use crate::clang_tools::clang_tidy::CompilationUnit; + +use git_bot_feedback::FileFilter; /// An enum to describe `--lines-changed-only` CLI option's behavior. #[derive(PartialEq, Clone, Debug, Default)] @@ -19,6 +21,16 @@ pub enum LinesChangedOnly { On, } +impl From for git_bot_feedback::LinesChangedOnly { + fn from(val: LinesChangedOnly) -> Self { + match val { + LinesChangedOnly::Off => git_bot_feedback::LinesChangedOnly::Off, + LinesChangedOnly::Diff => git_bot_feedback::LinesChangedOnly::Diff, + LinesChangedOnly::On => git_bot_feedback::LinesChangedOnly::On, + } + } +} + #[cfg(feature = "bin")] impl ValueEnum for LinesChangedOnly { /// Get a list possible value variants for display in `--help` output. @@ -172,6 +184,24 @@ pub struct ClangParams { impl From<&Cli> for ClangParams { /// Construct a [`ClangParams`] instance from a [`Cli`] instance. fn from(args: &Cli) -> Self { + let extensions: Vec<&str> = args + .source_options + .extensions + .iter() + .map(|ext| ext.as_str()) + .collect(); + let tidy_filter = args.tidy_options.ignore_tidy.as_ref().map(|ignore_tidy| { + let ignore_tidy: Vec<&str> = ignore_tidy.iter().map(|s| s.as_str()).collect(); + FileFilter::new(&ignore_tidy, &extensions.clone(), Some("clang-tidy")) + }); + let format_filter = args + .format_options + .ignore_format + .as_ref() + .map(|ignore_format| { + let ignore_format: Vec<&str> = ignore_format.iter().map(|s| s.as_str()).collect(); + FileFilter::new(&ignore_format, &extensions, Some("clang-format")) + }); ClangParams { tidy_checks: args.tidy_options.tidy_checks.clone(), lines_changed_only: args.source_options.lines_changed_only.clone(), @@ -181,16 +211,8 @@ impl From<&Cli> for ClangParams { style: args.format_options.style.clone(), clang_tidy_command: None, clang_format_command: None, - tidy_filter: args.tidy_options.ignore_tidy.as_ref().map(|ignore_tidy| { - FileFilter::new(ignore_tidy, args.source_options.extensions.clone()) - }), - format_filter: args - .format_options - .ignore_format - .as_ref() - .map(|ignore_format| { - FileFilter::new(ignore_format, args.source_options.extensions.clone()) - }), + tidy_filter, + format_filter, tidy_review: args.feedback_options.tidy_review, format_review: args.feedback_options.format_review, } @@ -243,17 +265,15 @@ impl Default for FeedbackInput { } } -#[cfg(test)] +#[cfg(all(test, feature = "bin"))] mod test { #![allow(clippy::unwrap_used)] - #[cfg(feature = "bin")] use clap::{Parser, ValueEnum}; use super::{Cli, LinesChangedOnly, ThreadComments}; #[test] - #[cfg(feature = "bin")] fn parse_positional() { let cli = Cli::parse_from(["cpp-linter", "file1.c", "file2.h"]); let not_ignored = cli.not_ignored.expect("failed to parse positional args"); diff --git a/cpp-linter/src/common_fs/file_filter.rs b/cpp-linter/src/common_fs/file_filter.rs deleted file mode 100644 index cb2dc84d..00000000 --- a/cpp-linter/src/common_fs/file_filter.rs +++ /dev/null @@ -1,281 +0,0 @@ -use anyhow::{Context, Result, anyhow}; -use fast_glob::glob_match; -use std::{ - fs, - path::{Path, PathBuf}, -}; - -use super::FileObj; - -/// A filter for files based on their path and extension. -#[derive(Debug, Clone)] -pub struct FileFilter { - pub ignored: Vec, - pub not_ignored: Vec, - pub extensions: Vec, -} -impl FileFilter { - /// Creates a new `FileFilter` from a list of ignore patterns and a list of extensions. - /// - /// The `ignore` parameter is a list of glob patterns that should be ignored. - /// These can be explicitly not ignored by prefixing them with `!`. - /// Hidden files/folders (patterns that start with a ".") cannot be explicitly not - /// ignored; they are always ignored. - /// - /// The `extensions` parameter is a list of file extensions that should be included. - pub fn new(ignore: &[String], extensions: Vec) -> Self { - let (ignored, not_ignored) = Self::parse_ignore(ignore); - Self { - ignored, - not_ignored, - extensions, - } - } - - /// This will parse the list of paths specified from the CLI using the `--ignore` - /// argument. - /// - /// It returns 2 lists (in order): - /// - /// - `ignored` paths - /// - `not_ignored` paths - fn parse_ignore(ignore: &[String]) -> (Vec, Vec) { - let mut ignored = vec![]; - let mut not_ignored = vec![]; - for pattern in ignore { - let as_posix = pattern.replace('\\', "/"); - let mut pat = as_posix.as_str().trim(); - let is_ignored = !pat.starts_with('!'); - if !is_ignored { - pat = pat[1..].trim_start(); - } - if pat.starts_with("./") { - pat = &pat[2..]; - } - let is_hidden = pat.starts_with('.'); - if is_hidden || is_ignored { - ignored.push(format!("./{pat}")); - } else { - not_ignored.push(format!("./{pat}")); - } - } - (ignored, not_ignored) - } - - /// This function will also read a .gitmodules file located in the working directory. - /// The named submodules' paths will be automatically added to the ignored list, - /// unless the submodule's path is already specified in the not_ignored list. - pub fn parse_submodules(&mut self) { - if let Ok(read_buf) = fs::read_to_string(".gitmodules") { - for line in read_buf.split('\n') { - if line.trim_start().starts_with("path") { - assert!(line.find('=').unwrap() > 0); - let submodule = - String::from("./") + line.split('=').next_back().unwrap().trim(); - log::debug!("Found submodule: {submodule}"); - let mut is_ignored = true; - for pat in &self.not_ignored { - if pat == &submodule { - is_ignored = false; - break; - } - } - if is_ignored && !self.ignored.contains(&submodule) { - self.ignored.push(submodule); - } - } - } - } - } - - /// Describes if a specified `file_name` is contained within the specified set of paths. - /// - /// The `is_ignored` flag describes which set of paths is used as domains. - /// The specified `file_name` can be a direct or distant descendant of any paths in - /// the set. - /// - /// Returns a `true` value of the the path/pattern that matches the given `file_name`. - /// If given `file_name` is not in the specified set, then `false` is returned. - pub fn is_file_in_list(&self, file_name: &Path, is_ignored: bool) -> bool { - let file_name = PathBuf::from(format!( - "./{}", - file_name - .as_os_str() - .to_string_lossy() - .to_string() - .replace("\\", "/") - .trim_start_matches("./") - )); - let set = if is_ignored { - &self.ignored - } else { - &self.not_ignored - }; - for pattern in set { - let glob_matched = - glob_match(pattern, file_name.to_string_lossy().to_string().as_str()); - let pat = PathBuf::from(&pattern); - if pattern.as_str() == "./" - || glob_matched - || (pat.is_file() && file_name == pat) - || (pat.is_dir() && file_name.starts_with(pat)) - { - log::debug!( - "file {file_name:?} is {}ignored with domain {pattern:?}.", - if is_ignored { "" } else { "not " } - ); - return true; - } - } - false - } - - /// A helper function that checks if `entry` satisfies the following conditions (in - /// ordered priority): - /// - /// - Does `entry`'s path use at least 1 of the listed file `extensions`? (takes - /// precedence) - /// - Is `entry` *not* specified in list of `ignored` paths? - /// - Is `entry` specified in the list of explicitly `not_ignored` paths? (supersedes - /// specified `ignored` paths) - pub fn is_source_or_ignored(&self, entry: &Path) -> bool { - let extension = entry - .extension() - .unwrap_or_default() // allow for matching files with no extension - .to_string_lossy() - .to_string(); - if !self.extensions.contains(&extension) { - return false; - } - let is_in_not_ignored = self.is_file_in_list(entry, false); - if is_in_not_ignored || !self.is_file_in_list(entry, true) { - return true; - } - false - } - - /// Walks a given `root_path` recursively and returns a [`Vec`] that - /// - /// - uses at least 1 of the given `extensions` - /// - is not specified in the internal list of `ignored` paths - /// - is specified in the internal list `not_ignored` paths (which supersedes `ignored` paths) - pub fn list_source_files(&self, root_path: &str) -> Result> { - let mut files: Vec = Vec::new(); - let entries = fs::read_dir(root_path) - .with_context(|| format!("Failed to read directory contents: {root_path}"))?; - for entry in entries.filter_map(|p| p.ok()) { - let path = entry.path(); - if path.is_dir() { - let mut is_hidden = false; - let parent = path - .components() - .next_back() - .ok_or(anyhow!("parent directory not known for {path:?}"))?; - if parent.as_os_str().to_str().unwrap().starts_with('.') { - is_hidden = true; - } - if !is_hidden { - files.extend(self.list_source_files(&path.to_string_lossy())?); - } - } else { - let is_valid_src = self.is_source_or_ignored(&path); - if is_valid_src { - files.push(FileObj::new( - path.clone().strip_prefix("./").unwrap().to_path_buf(), - )); - } - } - } - Ok(files) - } -} - -#[cfg(test)] -mod tests { - use clap::Parser; - - use super::FileFilter; - use crate::cli::Cli; - use std::{env::set_current_dir, path::PathBuf}; - - // ************* tests for ignored paths - - fn setup_ignore(input: &str, extension: Vec) -> FileFilter { - let args = Cli::parse_from(["cpp-linter", "-i", input]); - let ignore_arg = args.source_options.ignore; - let file_filter = FileFilter::new(&ignore_arg, extension); - println!("ignored = {:?}", file_filter.ignored); - println!("not ignored = {:?}", file_filter.not_ignored); - file_filter - } - - #[test] - fn ignore_src() { - let file_filter = setup_ignore("src", vec![]); - assert!(file_filter.is_file_in_list(&PathBuf::from("./src/lib.rs"), true)); - assert!(!file_filter.is_file_in_list(&PathBuf::from("./src/lib.rs"), false)); - } - - #[test] - fn ignore_root() { - let file_filter = setup_ignore("!src/lib.rs|./", vec![]); - assert!(file_filter.is_file_in_list(&PathBuf::from("./Cargo.toml"), true)); - assert!(file_filter.is_file_in_list(&PathBuf::from("./src/lib.rs"), false)); - } - - #[test] - fn ignore_root_implicit() { - let file_filter = setup_ignore("!src|", vec![]); - assert!(file_filter.is_file_in_list(&PathBuf::from("./Cargo.toml"), true)); - assert!(file_filter.is_file_in_list(&PathBuf::from("./src/lib.rs"), false)); - } - - #[test] - fn ignore_glob() { - let file_filter = setup_ignore("!src/**/*", vec![]); - assert!(file_filter.is_file_in_list(&PathBuf::from("./src/lib.rs"), false)); - assert!( - file_filter.is_file_in_list(&PathBuf::from("./src/common_fs/file_filter.rs"), false) - ); - } - - #[test] - fn ignore_submodules() { - set_current_dir("tests/ignored_paths").unwrap(); - let mut file_filter = setup_ignore("!pybind11", vec![]); - file_filter.parse_submodules(); - - // using Vec::contains() because these files don't actually exist in project files - for ignored_submodule in ["./RF24", "./RF24Network", "./RF24Mesh"] { - assert!(file_filter.ignored.contains(&ignored_submodule.to_string())); - assert!(!file_filter.is_file_in_list( - &PathBuf::from(ignored_submodule.to_string() + "/some_src.cpp"), - true - )); - } - assert!(file_filter.not_ignored.contains(&"./pybind11".to_string())); - assert!(!file_filter.is_file_in_list(&PathBuf::from("./pybind11/some_src.cpp"), false)); - } - - // *********************** tests for recursive path search - - #[test] - fn walk_dir_recursively() { - let extensions = vec!["cpp".to_string(), "hpp".to_string()]; - let file_filter = setup_ignore("target", extensions.clone()); - let files = file_filter.list_source_files(".").unwrap(); - assert!(!files.is_empty()); - for file in files { - assert!( - extensions.contains( - &file - .name - .extension() - .unwrap_or_default() - .to_string_lossy() - .to_string() - ) - ); - } - } -} diff --git a/cpp-linter/src/common_fs/mod.rs b/cpp-linter/src/common_fs/mod.rs index 0373b238..14fd3a17 100644 --- a/cpp-linter/src/common_fs/mod.rs +++ b/cpp-linter/src/common_fs/mod.rs @@ -1,18 +1,20 @@ //! A module to hold all common file system functionality. -use std::fmt::Debug; -use std::fs; -use std::path::Path; -use std::{ops::RangeInclusive, path::PathBuf}; - -use anyhow::{Context, Result}; - -use crate::clang_tools::clang_format::FormatAdvice; -use crate::clang_tools::clang_tidy::TidyAdvice; -use crate::clang_tools::{MakeSuggestions, ReviewComments, Suggestion, make_patch}; -use crate::cli::LinesChangedOnly; -mod file_filter; -pub use file_filter::FileFilter; +use std::{ + fmt::Debug, + fs, + ops::RangeInclusive, + path::{Path, PathBuf}, +}; + +use crate::{ + clang_tools::{ + MakeSuggestions, ReviewComments, Suggestion, clang_format::FormatAdvice, + clang_tidy::TidyAdvice, make_patch, + }, + cli::LinesChangedOnly, + error::FileObjError, +}; use git2::DiffHunk; /// A structure to represent a file's path and line changes. @@ -142,21 +144,22 @@ impl FileObj { &self, review_comments: &mut ReviewComments, summary_only: bool, - ) -> Result<()> { - let original_content = - fs::read(&self.name).with_context(|| "Failed to read original contents of file")?; + ) -> Result<(), FileObjError> { + let original_content = fs::read(&self.name).map_err(FileObjError::ReadFile)?; let file_name = self.name.to_str().unwrap_or_default().replace("\\", "/"); let file_path = Path::new(&file_name); if let Some(advice) = &self.format_advice && let Some(patched) = &advice.patched { - let mut patch = make_patch(file_path, patched, &original_content)?; + let mut patch = make_patch(file_path, patched, &original_content) + .map_err(|e| FileObjError::MakePatchFailed(file_name.clone(), e))?; advice.get_suggestions(review_comments, self, &mut patch, summary_only)?; } if let Some(advice) = &self.tidy_advice { if let Some(patched) = &advice.patched { - let mut patch = make_patch(file_path, patched, &original_content)?; + let mut patch = make_patch(file_path, patched, &original_content) + .map_err(|e| FileObjError::MakePatchFailed(file_name.clone(), e))?; advice.get_suggestions(review_comments, self, &mut patch, summary_only)?; } diff --git a/cpp-linter/src/error.rs b/cpp-linter/src/error.rs new file mode 100644 index 00000000..7c20d6aa --- /dev/null +++ b/cpp-linter/src/error.rs @@ -0,0 +1,66 @@ +use git_bot_feedback::RestClientError; + +#[derive(Debug, thiserror::Error)] +pub enum SuggestionError { + #[error("Failed to convert patch for '{file_name}' into bytes: {source}")] + PatchIntoBytesFailed { + file_name: String, + #[source] + source: git2::Error, + }, + #[error("Failed to convert patch for file '{file_name}' into string: {source}")] + PatchIntoStringFailed { + file_name: String, + #[source] + source: std::string::FromUtf8Error, + }, + #[error("Failed to get hunk {hunk_id} from patch for {file_name}: {source}")] + GetHunkFailed { + hunk_id: usize, + file_name: String, + #[source] + source: git2::Error, + }, + #[error( + "Failed to get line {line_index} in a hunk {hunk_id} of patch for {file_name}: {source}" + )] + GetHunkLineFailed { + line_index: usize, + hunk_id: usize, + file_name: String, + #[source] + source: git2::Error, + }, + #[error( + "Failed to convert line {line_index} buffer to string in hunk {hunk_id} of patch for {file_name}: {source}" + )] + HunkLineIntoStringFailed { + line_index: usize, + hunk_id: usize, + file_name: String, + #[source] + source: std::string::FromUtf8Error, + }, +} + +#[derive(Debug, thiserror::Error)] +pub enum FileObjError { + #[error("Failed to read file contents")] + ReadFile(std::io::Error), + #[error("Failed to create patch for file {0:?}: {1}")] + MakePatchFailed(String, #[source] git2::Error), + #[error(transparent)] + SuggestionError(#[from] SuggestionError), +} + +#[derive(Debug, thiserror::Error)] +pub enum ClientError { + #[error(transparent)] + RestClientError(#[from] RestClientError), + #[error("Unsupported Git server or CI platform")] + GitServerUnsupported, + #[error("Mutex lock poisoned for a source file: {0}")] + MutexPoisoned(String), + #[error(transparent)] + FileObjError(#[from] FileObjError), +} diff --git a/cpp-linter/src/git.rs b/cpp-linter/src/git.rs index 71cbea24..c93e1bf7 100644 --- a/cpp-linter/src/git.rs +++ b/cpp-linter/src/git.rs @@ -15,10 +15,8 @@ use anyhow::{Context, Result}; use git2::{Diff, Error, Patch, Repository}; // project specific modules/crates -use crate::{ - cli::LinesChangedOnly, - common_fs::{FileFilter, FileObj}, -}; +use crate::{cli::LinesChangedOnly, common_fs::FileObj}; +use git_bot_feedback::{FileFilter, error::DiffError}; /// This (re-)initializes the repository located in the specified `path`. /// @@ -174,7 +172,7 @@ pub fn parse_diff( if matches!( diff_delta.status(), git2::Delta::Added | git2::Delta::Modified | git2::Delta::Renamed, - ) && file_filter.is_source_or_ignored(&file_path) + ) && file_filter.is_qualified(&file_path) { let (added_lines, diff_chunks) = parse_patch(&Patch::from_diff(diff, file_idx).unwrap().unwrap()); @@ -198,246 +196,30 @@ pub fn parse_diff_from_buf( buff: &[u8], file_filter: &FileFilter, lines_changed_only: &LinesChangedOnly, -) -> Vec { +) -> Result, DiffError> { if let Ok(diff_obj) = &Diff::from_buffer(buff) { - parse_diff(diff_obj, file_filter, lines_changed_only) + Ok(parse_diff(diff_obj, file_filter, lines_changed_only)) } else { log::warn!("libgit2 failed to parse the diff"); - brute_force_parse_diff::parse_diff( + Ok(git_bot_feedback::parse_diff( &String::from_utf8_lossy(buff), file_filter, - lines_changed_only, - ) - } -} - -mod brute_force_parse_diff { - //! A private module to house the brute force algorithms of parsing a diff as a string. - //! This module is only intended as a fall back mechanism when [super::parse_diff_from_buf] - //! fails to use libgit2 C bindings. - //! - //! Since this is a fail safe, there are log messages that indicate when it is used. - //! Any instance where this mechanism is used should be reported as it is likely a bug - //! in libgit2 source. - - use regex::Regex; - use std::{ops::RangeInclusive, path::PathBuf}; - - use crate::{ - cli::LinesChangedOnly, - common_fs::{FileFilter, FileObj}, - }; - - fn get_filename_from_front_matter(front_matter: &str) -> Option<&str> { - let diff_file_name = Regex::new(r"(?m)^\+\+\+\sb?/(.*)$").unwrap(); - let diff_renamed_file = Regex::new(r"(?m)^rename to (.*)$").unwrap(); - let diff_binary_file = Regex::new(r"(?m)^Binary\sfiles\s").unwrap(); - if let Some(captures) = diff_file_name.captures(front_matter) { - return Some(captures.get(1).unwrap().as_str()); - } - if front_matter.trim_start().starts_with("similarity") - && let Some(captures) = diff_renamed_file.captures(front_matter) - { - return Some(captures.get(1).unwrap().as_str()); - } - if !diff_binary_file.is_match(front_matter) { - log::warn!("Unrecognized diff starting with:\n{}", front_matter); - } - None - } - - /// A regex pattern used in multiple functions - static HUNK_INFO_PATTERN: &str = r"(?m)@@\s\-\d+,\d+\s\+(\d+,\d+)\s@@"; - - /// Parses a single file's patch containing one or more hunks - /// Returns a 3-item tuple: - /// - the line numbers that contain additions - /// - the ranges of lines that span each hunk - fn parse_patch(patch: &str) -> (Vec, Vec>) { - let mut diff_chunks = Vec::new(); - let mut additions = Vec::new(); - - let hunk_info = Regex::new(HUNK_INFO_PATTERN).unwrap(); - if let Some(hunk_headers) = hunk_info.captures(patch) { - for (index, (hunk, header)) in - hunk_info.split(patch).zip(hunk_headers.iter()).enumerate() - { - if index == 0 { - continue; // we don't need the whole match, just the capture groups - } - let new_range: Vec = header - .unwrap() - .as_str() - .split(',') - .take(2) - .map(|val| val.parse::().unwrap()) - .collect(); - let start_line = new_range[0]; - let end_range = new_range[1]; - let mut line_numb_in_diff = start_line; - diff_chunks.push(RangeInclusive::new(start_line, start_line + end_range)); - for (line_index, line) in hunk.split('\n').enumerate() { - if line.starts_with('+') { - additions.push(line_numb_in_diff); - } - if line_index > 0 && !line.starts_with('-') { - line_numb_in_diff += 1; - } - } - } - } - (additions, diff_chunks) - } - - pub fn parse_diff( - diff: &str, - file_filter: &FileFilter, - lines_changed_only: &LinesChangedOnly, - ) -> Vec { - log::error!("Using brute force diff parsing!"); - let mut results = Vec::new(); - let diff_file_delimiter = Regex::new(r"(?m)^diff --git a/.*$").unwrap(); - let hunk_info = Regex::new(HUNK_INFO_PATTERN).unwrap(); - - let file_diffs = diff_file_delimiter.split(diff); - for file_diff in file_diffs { - if file_diff.is_empty() || file_diff.starts_with("deleted file") { - continue; - } - let hunk_start = if let Some(first_hunk) = hunk_info.find(file_diff) { - first_hunk.start() - } else { - file_diff.len() - }; - let front_matter = &file_diff[..hunk_start]; - if let Some(file_name) = get_filename_from_front_matter(front_matter) { - let file_path = PathBuf::from(file_name); - if file_filter.is_source_or_ignored(&file_path) { - let (added_lines, diff_chunks) = parse_patch(&file_diff[hunk_start..]); - if lines_changed_only - .is_change_valid(!added_lines.is_empty(), !diff_chunks.is_empty()) - { - results.push(FileObj::from(file_path, added_lines, diff_chunks)); - } - } - } - // } else { - // // file has no changed content. moving on - // continue; - // } - } - results - } - - // ******************* UNIT TESTS *********************** - #[cfg(test)] - mod test { - - use super::parse_diff; - use crate::{ - cli::LinesChangedOnly, - common_fs::{FileFilter, FileObj}, - git::parse_diff_from_buf, - }; - - static RENAMED_DIFF: &str = r#"diff --git a/tests/demo/some source.cpp b/tests/demo/some source.c -similarity index 100% -rename from /tests/demo/some source.cpp -rename to /tests/demo/some source.c -diff --git a/some picture.png b/some picture.png -new file mode 100644 -Binary files /dev/null and b/some picture.png differ -"#; - - static RENAMED_DIFF_WITH_CHANGES: &str = r#"diff --git a/tests/demo/some source.cpp b/tests/demo/some source.c -similarity index 99% -rename from /tests/demo/some source.cpp -rename to /tests/demo/some source.c -@@ -3,7 +3,7 @@ -\n \n \n-#include "iomanip" -+#include \n \n \n \n"#; - - #[test] - fn parse_renamed_diff() { - let diff_buf = RENAMED_DIFF.as_bytes(); - let files = parse_diff_from_buf( - diff_buf, - &FileFilter::new(&["target".to_string()], vec!["c".to_string()]), - &LinesChangedOnly::Off, - ); - assert!(!files.is_empty()); - assert!( - files - .first() - .unwrap() - .name - .ends_with("tests/demo/some source.c") - ); - } - - #[test] - fn parse_renamed_diff_with_patch() { - let diff_buf = RENAMED_DIFF_WITH_CHANGES.as_bytes(); - let files = parse_diff_from_buf( - diff_buf, - &FileFilter::new(&["target".to_string()], vec!["c".to_string()]), - &LinesChangedOnly::Off, - ); - assert!(!files.is_empty()); - } - - /// Used to parse the same string buffer using both libgit2 and brute force regex. - /// Returns 2 vectors of [FileObj] that should be equivalent. - fn setup_parsed(buf: &str, extensions: &[String]) -> (Vec, Vec) { - let ignore = ["target".to_string()]; - ( - parse_diff_from_buf( - buf.as_bytes(), - &FileFilter::new(&ignore, extensions.to_owned()), - &LinesChangedOnly::Off, - ), - parse_diff( - buf, - &FileFilter::new(&ignore, extensions.to_owned()), - &LinesChangedOnly::Off, - ), + &lines_changed_only.clone().into(), + )? + .iter() + .map(|(name, diff_lines)| { + let diff_chunks = diff_lines + .diff_hunks + .iter() + .map(|hunk| hunk.start..=hunk.end) + .collect(); + FileObj::from( + PathBuf::from(&name), + diff_lines.added_lines.clone(), + diff_chunks, ) - } - - fn assert_files_eq(files_from_a: &[FileObj], files_from_b: &[FileObj]) { - assert_eq!(files_from_a.len(), files_from_b.len()); - for (a, b) in files_from_a.iter().zip(files_from_b) { - assert_eq!(a.name, b.name); - assert_eq!(a.added_lines, b.added_lines); - assert_eq!(a.added_ranges, b.added_ranges); - assert_eq!(a.diff_chunks, b.diff_chunks); - } - } - - #[test] - fn parse_typical_diff() { - let diff_buf = "diff --git a/path/for/Some file.cpp b/path/to/Some file.cpp\n\ - --- a/path/for/Some file.cpp\n\ - +++ b/path/to/Some file.cpp\n\ - @@ -3,7 +3,7 @@\n \n \n \n\ - -#include \n\ - +#include \n \n \n \n"; - - let (files_from_buf, files_from_str) = setup_parsed(diff_buf, &[String::from("cpp")]); - assert!(!files_from_buf.is_empty()); - assert_files_eq(&files_from_buf, &files_from_str); - } - - #[test] - fn parse_binary_diff() { - let diff_buf = "diff --git a/some picture.png b/some picture.png\n\ - new file mode 100644\n\ - Binary files /dev/null and b/some picture.png differ\n"; - - let (files_from_buf, files_from_str) = setup_parsed(diff_buf, &[String::from("png")]); - assert!(files_from_buf.is_empty()); - assert_files_eq(&files_from_buf, &files_from_str); - } + }) + .collect()) } } @@ -452,11 +234,8 @@ mod test { use tempfile::{TempDir, tempdir}; use super::get_sha; - use crate::{ - cli::LinesChangedOnly, - common_fs::FileFilter, - rest_api::{RestApiClient, github::GithubApiClient}, - }; + use crate::{cli::LinesChangedOnly, rest_client::RestClient}; + use git_bot_feedback::FileFilter; const TEST_REPO_URL: &str = "https://github.com/cpp-linter/cpp-linter"; @@ -502,19 +281,28 @@ mod test { tmp.path().as_os_str().to_str().unwrap(), patch_path, ); - let rest_api_client = GithubApiClient::new(); - let file_filter = FileFilter::new(&["target".to_string()], extensions.to_owned()); - set_current_dir(tmp).unwrap(); // avoid use of REST API when testing in CI unsafe { + env::set_var("GITHUB_ACTIONS", "false"); env::set_var("CI", "false"); } + let rest_api_client = RestClient::new().unwrap(); + let file_filter = FileFilter::new( + &["target"], + &extensions.iter().map(|s| s.as_str()).collect::>(), + None, + ); + set_current_dir(tmp).unwrap(); + let base_diff = if ignore_staged { + Some("0".to_string()) + } else { + None:: + }; rest_api_client - .unwrap() .get_list_of_changed_files( &file_filter, - &LinesChangedOnly::Off, - if ignore_staged { &Some(0) } else { &None:: }, + &LinesChangedOnly::Off.into(), + &base_diff, ignore_staged, ) .await diff --git a/cpp-linter/src/lib.rs b/cpp-linter/src/lib.rs index 60bf0d6a..f7250a6c 100644 --- a/cpp-linter/src/lib.rs +++ b/cpp-linter/src/lib.rs @@ -10,7 +10,8 @@ pub mod clang_tools; pub mod cli; pub mod common_fs; +pub mod error; pub mod git; pub mod logger; -pub mod rest_api; +pub mod rest_client; pub mod run; diff --git a/cpp-linter/src/rest_api/github/mod.rs b/cpp-linter/src/rest_api/github/mod.rs deleted file mode 100644 index 47288961..00000000 --- a/cpp-linter/src/rest_api/github/mod.rs +++ /dev/null @@ -1,450 +0,0 @@ -//! This module holds functionality specific to using Github's REST API. -//! -//! In the root module, we just implement the RestApiClient trait. -//! In other (private) submodules we implement behavior specific to Github's REST API. - -use std::env; -use std::fmt::Display; -use std::fs::OpenOptions; -use std::io::Write; -use std::sync::{Arc, Mutex}; - -// non-std crates -use anyhow::{Context, Result}; -use reqwest::{ - Client, Method, Url, - header::{AUTHORIZATION, HeaderMap, HeaderValue}, -}; - -// project specific modules/crates -use super::{RestApiClient, RestApiRateLimitHeaders, send_api_request}; -use crate::clang_tools::ClangVersions; -use crate::clang_tools::clang_format::tally_format_advice; -use crate::clang_tools::clang_tidy::tally_tidy_advice; -use crate::cli::{FeedbackInput, LinesChangedOnly, ThreadComments}; -use crate::common_fs::{FileFilter, FileObj}; -use crate::git::{get_diff, open_repo, parse_diff, parse_diff_from_buf}; - -// private submodules. -mod serde_structs; -mod specific_api; - -/// A structure to work with Github REST API. -pub struct GithubApiClient { - /// The HTTP request client to be used for all REST API calls. - client: Client, - - /// The CI run's event payload from the webhook that triggered the workflow. - pull_request: i64, - - /// The name of the event that was triggered when running cpp_linter. - pub event_name: String, - - /// The value of the `GITHUB_API_URL` environment variable. - api_url: Url, - - /// The value of the `GITHUB_REPOSITORY` environment variable. - repo: Option, - - /// The value of the `GITHUB_SHA` environment variable. - sha: Option, - - /// The value of the `ACTIONS_STEP_DEBUG` environment variable. - pub debug_enabled: bool, - - /// The response header names that describe the rate limit status. - rate_limit_headers: RestApiRateLimitHeaders, -} - -// implement the RestApiClient trait for the GithubApiClient -impl RestApiClient for GithubApiClient { - fn set_exit_code( - &self, - checks_failed: u64, - format_checks_failed: Option, - tidy_checks_failed: Option, - ) -> u64 { - if let Ok(gh_out) = env::var("GITHUB_OUTPUT") { - if let Ok(mut gh_out_file) = OpenOptions::new().append(true).open(gh_out) { - for (prompt, value) in [ - ("checks-failed", Some(checks_failed)), - ("format-checks-failed", format_checks_failed), - ("tidy-checks-failed", tidy_checks_failed), - ] { - if let Err(e) = writeln!(gh_out_file, "{prompt}={}", value.unwrap_or(0),) { - log::error!("Could not write to GITHUB_OUTPUT file: {}", e); - break; - } - } - if let Err(e) = gh_out_file.flush() { - log::debug!("Failed to flush buffer to GITHUB_OUTPUT file: {e:?}"); - } - } else { - log::debug!("GITHUB_OUTPUT file could not be opened"); - } - } - log::info!( - "{} clang-format-checks-failed", - format_checks_failed.unwrap_or(0) - ); - log::info!( - "{} clang-tidy-checks-failed", - tidy_checks_failed.unwrap_or(0) - ); - log::info!("{checks_failed} checks-failed"); - checks_failed - } - - /// This prints a line to indicate the beginning of a related group of log statements. - fn start_log_group(&self, name: String) { - log::info!(target: "CI_LOG_GROUPING", "::group::{}", name); - } - - /// This prints a line to indicate the ending of a related group of log statements. - fn end_log_group(&self) { - log::info!(target: "CI_LOG_GROUPING", "::endgroup::"); - } - - fn make_headers() -> Result> { - let mut headers = HeaderMap::new(); - headers.insert( - "Accept", - HeaderValue::from_str("application/vnd.github.raw+json")?, - ); - if let Ok(token) = env::var("GITHUB_TOKEN") { - log::debug!("Using auth token from GITHUB_TOKEN environment variable"); - let mut val = HeaderValue::from_str(format!("token {token}").as_str())?; - val.set_sensitive(true); - headers.insert(AUTHORIZATION, val); - } - Ok(headers) - } - - async fn get_list_of_changed_files( - &self, - file_filter: &FileFilter, - lines_changed_only: &LinesChangedOnly, - diff_base: &Option, - ignore_index: bool, - ) -> Result> { - if env::var("CI").is_ok_and(|val| val.as_str() == "true") - && let Some(repo) = self.repo.as_ref() - { - // get diff from Github REST API - let is_pr = self.event_name == "pull_request"; - let pr = self.pull_request.to_string(); - let sha = self.sha.clone().unwrap_or_default(); - let url = self - .api_url - .join(format!("repos/{repo}/").as_str())? - .join(if is_pr { "pulls/" } else { "commits/" })? - .join(if is_pr { pr.as_str() } else { sha.as_str() })?; - let mut diff_header = HeaderMap::new(); - diff_header.insert("Accept", "application/vnd.github.diff".parse()?); - log::debug!("Getting file changes from {}", url.as_str()); - let request = Self::make_api_request( - &self.client, - url.as_str(), - Method::GET, - None, - Some(diff_header), - )?; - let response = send_api_request(&self.client, request, &self.rate_limit_headers) - .await - .with_context(|| "Failed to get list of changed files.")?; - if response.status().is_success() { - Ok(parse_diff_from_buf( - &response.bytes().await?, - file_filter, - lines_changed_only, - )) - } else { - let endpoint = if is_pr { - Url::parse(format!("{}/files", url.as_str()).as_str())? - } else { - url - }; - Self::log_response(response, "Failed to get full diff for event").await; - log::debug!("Trying paginated request to {}", endpoint.as_str()); - self.get_changed_files_paginated(endpoint, file_filter, lines_changed_only) - .await - } - } else { - // get diff from libgit2 API - let repo = open_repo(".").with_context( - || "Please ensure the repository is checked out before running cpp-linter.", - )?; - let list = parse_diff( - &get_diff(&repo, diff_base, ignore_index)?, - file_filter, - lines_changed_only, - ); - Ok(list) - } - } - - async fn post_feedback( - &self, - files: &[Arc>], - feedback_inputs: FeedbackInput, - clang_versions: ClangVersions, - ) -> Result { - let tidy_checks_failed = tally_tidy_advice(files)?; - let format_checks_failed = tally_format_advice(files)?; - let mut comment = None; - - if feedback_inputs.file_annotations { - self.post_annotations(files, feedback_inputs.style.as_str()); - } - if feedback_inputs.step_summary { - comment = Some(Self::make_comment( - files, - format_checks_failed, - tidy_checks_failed, - &clang_versions, - None, - )); - self.post_step_summary(comment.as_ref().unwrap()); - } - self.set_exit_code( - format_checks_failed + tidy_checks_failed, - Some(format_checks_failed), - Some(tidy_checks_failed), - ); - - if feedback_inputs.thread_comments != ThreadComments::Off { - // post thread comment for PR or push event - if comment.as_ref().is_some_and(|c| c.len() > 65535) || comment.is_none() { - comment = Some(Self::make_comment( - files, - format_checks_failed, - tidy_checks_failed, - &clang_versions, - Some(65535), - )); - } - if let Some(repo) = &self.repo { - let is_pr = self.event_name == "pull_request"; - let pr = self.pull_request.to_string() + "/"; - let sha = self.sha.clone().unwrap() + "/"; - let comments_url = self - .api_url - .join("repos/")? - .join(format!("{}/", repo).as_str())? - .join(if is_pr { "issues/" } else { "commits/" })? - .join(if is_pr { pr.as_str() } else { sha.as_str() })? - .join("comments")?; - - self.update_comment( - comments_url, - &comment.unwrap(), - feedback_inputs.no_lgtm, - format_checks_failed + tidy_checks_failed == 0, - feedback_inputs.thread_comments == ThreadComments::Update, - ) - .await?; - } - } - if self.event_name == "pull_request" - && (feedback_inputs.tidy_review || feedback_inputs.format_review) - { - self.post_review(files, &feedback_inputs, &clang_versions) - .await?; - } - Ok(format_checks_failed + tidy_checks_failed) - } -} - -#[cfg(test)] -mod test { - use std::{ - default::Default, - env, - io::Read, - path::{Path, PathBuf}, - sync::{Arc, Mutex}, - }; - - use regex::Regex; - use semver::Version; - use tempfile::{NamedTempFile, tempdir}; - - use super::GithubApiClient; - use crate::{ - clang_tools::{ - ClangVersions, - clang_format::{FormatAdvice, Replacement}, - clang_tidy::{TidyAdvice, TidyNotification}, - }, - cli::{FeedbackInput, LinesChangedOnly}, - common_fs::{FileFilter, FileObj}, - logger, - rest_api::{RestApiClient, USER_OUTREACH}, - }; - - // ************************* tests for step-summary and output variables - - async fn create_comment( - is_lgtm: bool, - fail_gh_out: bool, - fail_summary: bool, - ) -> (String, String) { - let tmp_dir = tempdir().unwrap(); - let rest_api_client = GithubApiClient::new().unwrap(); - logger::try_init(); - if env::var("ACTIONS_STEP_DEBUG").is_ok_and(|var| var == "true") { - assert!(rest_api_client.debug_enabled); - log::set_max_level(log::LevelFilter::Debug); - } - let mut files = vec![]; - if !is_lgtm { - for _i in 0..65535 { - let filename = String::from("tests/demo/demo.cpp"); - let mut file = FileObj::new(PathBuf::from(&filename)); - let notes = vec![TidyNotification { - filename, - line: 0, - cols: 0, - severity: String::from("note"), - rationale: String::from("A test dummy rationale"), - diagnostic: String::from("clang-diagnostic-warning"), - suggestion: vec![], - fixed_lines: vec![], - }]; - file.tidy_advice = Some(TidyAdvice { - notes, - patched: None, - }); - file.format_advice = Some(FormatAdvice { - replacements: vec![Replacement { offset: 0, line: 1 }], - patched: None, - }); - files.push(Arc::new(Mutex::new(file))); - } - } - let feedback_inputs = FeedbackInput { - style: if is_lgtm { - String::new() - } else { - String::from("file") - }, - step_summary: true, - ..Default::default() - }; - let mut step_summary_path = NamedTempFile::new_in(tmp_dir.path()).unwrap(); - let mut gh_out_path = NamedTempFile::new_in(tmp_dir.path()).unwrap(); - unsafe { - env::set_var( - "GITHUB_STEP_SUMMARY", - if fail_summary { - Path::new("not-a-file.txt") - } else { - step_summary_path.path() - }, - ); - env::set_var( - "GITHUB_OUTPUT", - if fail_gh_out { - Path::new("not-a-file.txt") - } else { - gh_out_path.path() - }, - ); - } - let clang_versions = ClangVersions { - format_version: Some(Version::new(1, 2, 3)), - tidy_version: Some(Version::new(1, 2, 3)), - }; - rest_api_client - .post_feedback(&files, feedback_inputs, clang_versions) - .await - .unwrap(); - let mut step_summary_content = String::new(); - step_summary_path - .read_to_string(&mut step_summary_content) - .unwrap(); - if !fail_summary { - assert!(&step_summary_content.contains(USER_OUTREACH)); - } - let mut gh_out_content = String::new(); - gh_out_path.read_to_string(&mut gh_out_content).unwrap(); - if !fail_gh_out { - assert!(gh_out_content.starts_with("checks-failed=")); - } - (step_summary_content, gh_out_content) - } - - #[tokio::test] - async fn check_comment_concerns() { - let (comment, gh_out) = create_comment(false, false, false).await; - assert!(&comment.contains(":warning:\nSome files did not pass the configured checks!\n")); - let fmt_pattern = Regex::new(r"format-checks-failed=(\d+)\n").unwrap(); - let tidy_pattern = Regex::new(r"tidy-checks-failed=(\d+)\n").unwrap(); - for pattern in [fmt_pattern, tidy_pattern] { - let number = pattern - .captures(&gh_out) - .expect("found no number of checks-failed") - .get(1) - .unwrap() - .as_str() - .parse::() - .unwrap(); - assert!(number > 0); - } - } - - #[tokio::test] - async fn check_comment_lgtm() { - unsafe { - env::set_var("ACTIONS_STEP_DEBUG", "true"); - } - let (comment, gh_out) = create_comment(true, false, false).await; - assert!(comment.contains(":heavy_check_mark:\nNo problems need attention.")); - assert_eq!( - gh_out, - "checks-failed=0\nformat-checks-failed=0\ntidy-checks-failed=0\n" - ); - } - - #[tokio::test] - async fn fail_gh_output() { - unsafe { - env::set_var("ACTIONS_STEP_DEBUG", "true"); - } - let (comment, gh_out) = create_comment(true, true, false).await; - assert!(&comment.contains(":heavy_check_mark:\nNo problems need attention.")); - assert!(gh_out.is_empty()); - } - - #[tokio::test] - async fn fail_gh_summary() { - unsafe { - env::set_var("ACTIONS_STEP_DEBUG", "true"); - } - let (comment, gh_out) = create_comment(true, false, true).await; - assert!(comment.is_empty()); - assert_eq!( - gh_out, - "checks-failed=0\nformat-checks-failed=0\ntidy-checks-failed=0\n" - ); - } - - #[tokio::test] - async fn fail_get_local_diff() { - unsafe { - env::set_var("CI", "false"); - } - let tmp_dir = tempdir().unwrap(); - env::set_current_dir(tmp_dir.path()).unwrap(); - let rest_client = GithubApiClient::new().unwrap(); - let files = rest_client - .get_list_of_changed_files( - &FileFilter::new(&[], vec![]), - &LinesChangedOnly::Off, - &None::, - false, - ) - .await; - assert!(files.is_err()) - } -} diff --git a/cpp-linter/src/rest_api/github/serde_structs.rs b/cpp-linter/src/rest_api/github/serde_structs.rs deleted file mode 100644 index e34e3a09..00000000 --- a/cpp-linter/src/rest_api/github/serde_structs.rs +++ /dev/null @@ -1,107 +0,0 @@ -//! This submodule declares data structures used to -//! deserialize (and serializer) JSON payload data. - -use serde::{Deserialize, Serialize}; - -use crate::clang_tools::Suggestion; -use crate::rest_api::COMMENT_MARKER; - -#[derive(Debug, Serialize)] -pub struct FullReview { - pub event: String, - pub body: String, - pub comments: Vec, -} - -#[derive(Debug, Serialize)] -pub struct ReviewDiffComment { - pub body: String, - pub line: i64, - #[serde(skip_serializing_if = "Option::is_none")] - pub start_line: Option, - pub path: String, -} - -impl From for ReviewDiffComment { - fn from(value: Suggestion) -> Self { - Self { - body: format!("{COMMENT_MARKER}{}", value.suggestion), - line: value.line_end as i64, - start_line: if value.line_end != value.line_start { - Some(value.line_start as i64) - } else { - None - }, - path: value.path, - } - } -} - -/// A constant string used as a payload to dismiss PR reviews. -pub const REVIEW_DISMISSAL: &str = r#"{"event":"DISMISS","message":"outdated suggestion"}"#; - -/// A structure for deserializing a single changed file in a CI event. -#[derive(Debug, Deserialize, PartialEq, Clone)] -pub struct GithubChangedFile { - /// The file's name (including relative path to repo root) - pub filename: String, - /// If renamed, this will be the file's old name as a [`Some`], otherwise [`None`]. - pub previous_filename: Option, - /// The individual patch that describes the file's changes. - pub patch: Option, - /// The number of changes to the file contents. - pub changes: i64, -} - -/// A structure for deserializing a Push event's changed files. -#[derive(Debug, Deserialize, PartialEq, Clone)] -pub struct PushEventFiles { - /// The list of changed files. - pub files: Vec, -} - -/// A structure for deserializing a comment from a response's json. -#[derive(Debug, Deserialize, PartialEq, Clone)] -pub struct PullRequestInfo { - /// Is this PR a draft? - pub draft: bool, - /// What is current state of this PR? - /// - /// Here we only care if it is `"open"`. - pub state: String, -} - -/// A structure for deserializing a comment from a response's json. -#[derive(Debug, Deserialize, PartialEq, Clone)] -pub struct ReviewComment { - /// The content of the review's summary comment. - pub body: Option, - /// The review's ID. - pub id: i64, - /// The state of the review in question. - /// - /// This could be "PENDING", "DISMISSED", "APPROVED", or "COMMENT". - pub state: String, -} - -/// A structure for deserializing a comment from a response's json. -#[derive(Debug, Deserialize, PartialEq, Clone)] -pub struct ThreadComment { - /// The comment's ID number. - pub id: i64, - /// The comment's body number. - pub body: String, - /// The comment's user number. - /// - /// This is only used for debug output. - pub user: User, -} - -/// A structure for deserializing a comment's author from a response's json. -/// -/// This is only used for debug output. -#[derive(Debug, Deserialize, PartialEq, Clone)] -pub struct User { - pub login: String, - pub id: u64, -} diff --git a/cpp-linter/src/rest_api/github/specific_api.rs b/cpp-linter/src/rest_api/github/specific_api.rs deleted file mode 100644 index 4f7a17f7..00000000 --- a/cpp-linter/src/rest_api/github/specific_api.rs +++ /dev/null @@ -1,543 +0,0 @@ -//! This submodule implements functionality exclusively specific to Github's REST API. - -use std::{ - collections::HashMap, - env, - fs::OpenOptions, - io::{Read, Write}, - path::{Path, PathBuf}, - sync::{Arc, Mutex}, -}; - -use anyhow::{Context, Result, anyhow}; -use reqwest::{Client, Method, Url}; - -use crate::{ - clang_tools::{ClangVersions, ReviewComments, clang_format::summarize_style}, - cli::{FeedbackInput, LinesChangedOnly}, - common_fs::{FileFilter, FileObj}, - git::parse_diff_from_buf, - rest_api::{COMMENT_MARKER, RestApiRateLimitHeaders, USER_AGENT, send_api_request}, -}; - -use super::{ - GithubApiClient, RestApiClient, - serde_structs::{ - FullReview, GithubChangedFile, PullRequestInfo, PushEventFiles, REVIEW_DISMISSAL, - ReviewComment, ReviewDiffComment, ThreadComment, - }, -}; - -impl GithubApiClient { - /// Instantiate a [`GithubApiClient`] object. - pub fn new() -> Result { - let event_name = env::var("GITHUB_EVENT_NAME").unwrap_or(String::from("unknown")); - let pull_request = { - match event_name.as_str() { - "pull_request" => { - // GITHUB_*** env vars cannot be overwritten in CI runners on GitHub. - let event_payload_path = env::var("GITHUB_EVENT_PATH")?; - // event payload JSON file can be overwritten/removed in CI runners - let file_buf = &mut String::new(); - OpenOptions::new() - .read(true) - .open(event_payload_path.clone())? - .read_to_string(file_buf) - .with_context(|| { - format!("Failed to read event payload at {event_payload_path}") - })?; - let payload = - serde_json::from_str::>( - file_buf, - ) - .with_context(|| "Failed to deserialize event payload")?; - payload["number"].as_i64().unwrap_or(-1) - } - _ => -1, - } - }; - // GITHUB_*** env vars cannot be overwritten in CI runners on GitHub. - let gh_api_url = env::var("GITHUB_API_URL").unwrap_or("https://api.github.com".to_string()); - let api_url = Url::parse(gh_api_url.as_str())?; - - Ok(GithubApiClient { - client: Client::builder() - .default_headers(Self::make_headers()?) - .user_agent(USER_AGENT) - .build()?, - pull_request, - event_name, - api_url, - repo: env::var("GITHUB_REPOSITORY").ok(), - sha: env::var("GITHUB_SHA").ok(), - debug_enabled: env::var("ACTIONS_STEP_DEBUG").is_ok_and(|val| &val == "true"), - rate_limit_headers: RestApiRateLimitHeaders { - reset: "x-ratelimit-reset".to_string(), - remaining: "x-ratelimit-remaining".to_string(), - retry: "retry-after".to_string(), - }, - }) - } - - /// A way to get the list of changed files using REST API calls that employ a paginated response. - /// - /// This is a helper to [`Self::get_list_of_changed_files()`] but takes a formulated `url` - /// endpoint based on the context of the triggering CI event. - pub(super) async fn get_changed_files_paginated( - &self, - url: Url, - file_filter: &FileFilter, - lines_changed_only: &LinesChangedOnly, - ) -> Result> { - let mut url = Some(Url::parse_with_params(url.as_str(), &[("page", "1")])?); - let mut files = vec![]; - while let Some(ref endpoint) = url { - let request = - Self::make_api_request(&self.client, endpoint.as_str(), Method::GET, None, None)?; - let response = send_api_request(&self.client, request, &self.rate_limit_headers) - .await - .with_context(|| "Failed to get paginated list of changed files")?; - url = Self::try_next_page(response.headers()); - let files_list = if self.event_name != "pull_request" { - let json_value: PushEventFiles = serde_json::from_str(&response.text().await?) - .with_context( - || "Failed to deserialize list of changed files from json response", - )?; - json_value.files - } else { - serde_json::from_str::>(&response.text().await?) - .with_context( - || "Failed to deserialize list of file changes from Pull Request event.", - )? - }; - for file in files_list { - let ext = Path::new(&file.filename).extension().unwrap_or_default(); - if !file_filter - .extensions - .contains(&ext.to_string_lossy().to_string()) - { - continue; - } - if let Some(patch) = file.patch { - let diff = format!( - "diff --git a/{old} b/{new}\n--- a/{old}\n+++ b/{new}\n{patch}\n", - old = file.previous_filename.unwrap_or(file.filename.clone()), - new = file.filename, - ); - if let Some(file_obj) = - parse_diff_from_buf(diff.as_bytes(), file_filter, lines_changed_only) - .first() - { - files.push(file_obj.to_owned()); - } - } else if file.changes == 0 { - // file may have been only renamed. - // include it in case files-changed-only is enabled. - files.push(FileObj::new(PathBuf::from(file.filename))); - } - // else changes are too big or we don't care - } - } - Ok(files) - } - - /// Append step summary to CI workflow's summary page. - pub fn post_step_summary(&self, comment: &String) { - if let Ok(gh_out) = env::var("GITHUB_STEP_SUMMARY") { - // step summary MD file can be overwritten/removed in CI runners - if let Ok(mut gh_out_file) = OpenOptions::new().append(true).open(gh_out) { - if let Err(e) = writeln!(gh_out_file, "\n{}\n", comment) { - log::error!("Could not write to GITHUB_STEP_SUMMARY file: {}", e); - } - } else { - log::error!("GITHUB_STEP_SUMMARY file could not be opened"); - } - } - } - - /// Post file annotations. - pub fn post_annotations(&self, files: &[Arc>], style: &str) { - let style_guide = summarize_style(style); - - // iterate over clang-format advice and post annotations - for file in files { - let file = file.lock().unwrap(); - if let Some(format_advice) = &file.format_advice { - // assemble a list of line numbers - let mut lines = Vec::new(); - for replacement in &format_advice.replacements { - if !lines.contains(&replacement.line) { - lines.push(replacement.line); - } - } - // post annotation if any applicable lines were formatted - if !lines.is_empty() { - println!( - "::notice file={name},title=Run clang-format on {name}::File {name} does not conform to {style_guide} style guidelines. (lines {line_set})", - name = &file.name.to_string_lossy().replace('\\', "/"), - line_set = lines - .iter() - .map(|val| val.to_string()) - .collect::>() - .join(","), - ); - } - } // end format_advice iterations - - // iterate over clang-tidy advice and post annotations - // The tidy_advice vector is parallel to the files vector; meaning it serves as a file filterer. - // lines are already filter as specified to clang-tidy CLI. - if let Some(tidy_advice) = &file.tidy_advice { - for note in &tidy_advice.notes { - if note.filename == file.name.to_string_lossy().replace('\\', "/") { - println!( - "::{severity} file={file},line={line},title={file}:{line}:{cols} [{diag}]::{info}", - severity = if note.severity == *"note" { - "notice".to_string() - } else { - note.severity.clone() - }, - file = note.filename, - line = note.line, - cols = note.cols, - diag = note.diagnostic, - info = note.rationale, - ); - } - } - } - } - } - - /// Update existing comment or remove old comment(s) and post a new comment - pub async fn update_comment( - &self, - url: Url, - comment: &String, - no_lgtm: bool, - is_lgtm: bool, - update_only: bool, - ) -> Result<()> { - let comment_url = self - .remove_bot_comments(&url, !update_only || (is_lgtm && no_lgtm)) - .await?; - if !is_lgtm || !no_lgtm { - let payload = HashMap::from([("body", comment)]); - // log::debug!("payload body:\n{:?}", payload); - let req_meth = if comment_url.is_some() { - Method::PATCH - } else { - Method::POST - }; - let request = Self::make_api_request( - &self.client, - comment_url.unwrap_or(url), - req_meth, - Some(serde_json::json!(&payload).to_string()), - None, - )?; - match send_api_request(&self.client, request, &self.rate_limit_headers).await { - Ok(response) => { - Self::log_response(response, "Failed to post thread comment").await; - } - Err(e) => { - log::error!("Failed to post thread comment: {e:?}"); - } - } - } - Ok(()) - } - - /// Remove thread comments previously posted by cpp-linter. - async fn remove_bot_comments(&self, url: &Url, delete: bool) -> Result> { - let mut comment_url = None; - let mut comments_url = Some(Url::parse_with_params(url.as_str(), &[("page", "1")])?); - let repo = format!( - "repos/{}{}/comments", - // if we got here, then we know it is on a CI runner as self.repo should be known - self.repo.as_ref().expect("Repo name unknown."), - if self.event_name == "pull_request" { - "/issues" - } else { - "" - }, - ); - let base_comment_url = self.api_url.join(&repo).unwrap(); - while let Some(ref endpoint) = comments_url { - let request = - Self::make_api_request(&self.client, endpoint.as_str(), Method::GET, None, None)?; - let result = send_api_request(&self.client, request, &self.rate_limit_headers).await; - match result { - Err(e) => { - log::error!("Failed to get list of existing thread comments: {e:?}"); - return Ok(comment_url); - } - Ok(response) => { - if !response.status().is_success() { - Self::log_response( - response, - "Failed to get list of existing thread comments", - ) - .await; - return Ok(comment_url); - } - comments_url = Self::try_next_page(response.headers()); - let payload = - serde_json::from_str::>(&response.text().await?); - match payload { - Err(e) => { - log::error!( - "Failed to deserialize list of existing thread comments: {e:?}" - ); - continue; - } - Ok(payload) => { - for comment in payload { - if comment.body.starts_with(COMMENT_MARKER) { - log::debug!( - "Found cpp-linter comment id {} from user {} ({})", - comment.id, - comment.user.login, - comment.user.id, - ); - let this_comment_url = Url::parse( - format!("{base_comment_url}/{}", comment.id).as_str(), - )?; - if delete || comment_url.is_some() { - // if not updating: remove all outdated comments - // if updating: remove all outdated comments except the last one - - // use last saved comment_url (if not None) or current comment url - let del_url = if let Some(last_url) = &comment_url { - last_url - } else { - &this_comment_url - }; - let req = Self::make_api_request( - &self.client, - del_url.as_str(), - Method::DELETE, - None, - None, - )?; - match send_api_request( - &self.client, - req, - &self.rate_limit_headers, - ) - .await - { - Ok(result) => { - if !result.status().is_success() { - Self::log_response( - result, - "Failed to delete old thread comment", - ) - .await; - } - } - Err(e) => { - log::error!( - "Failed to delete old thread comment: {e:?}" - ) - } - } - } - if !delete { - comment_url = Some(this_comment_url) - } - } - } - } - } - } - } - } - Ok(comment_url) - } - - /// Post a PR review with code suggestions. - /// - /// Note: `--no-lgtm` is applied when nothing is suggested. - pub async fn post_review( - &self, - files: &[Arc>], - feedback_input: &FeedbackInput, - clang_versions: &ClangVersions, - ) -> Result<()> { - let url = self - .api_url - .join("repos/")? - .join( - format!( - "{}/", - // if we got here, then we know self.repo should be known - self.repo.as_ref().ok_or(anyhow!("Repo name unknown"))? - ) - .as_str(), - )? - .join("pulls/")? - // if we got here, then we know that it is a self.pull_request is a valid value - .join(self.pull_request.to_string().as_str())?; - let request = Self::make_api_request(&self.client, url.as_str(), Method::GET, None, None)?; - let response = send_api_request(&self.client, request, &self.rate_limit_headers); - - let url = Url::parse(format!("{}/", url).as_str())?.join("reviews")?; - let dismissal = self.dismiss_outdated_reviews(&url); - match response.await { - Ok(response) => { - match serde_json::from_str::(&response.text().await?) { - Err(e) => { - log::error!("Failed to deserialize PR info: {e:?}"); - return dismissal.await; - } - Ok(pr_info) => { - if pr_info.draft || pr_info.state != "open" { - return dismissal.await; - } - } - } - } - Err(e) => { - log::error!("Failed to get PR info from {e:?}"); - return dismissal.await; - } - } - - let summary_only = ["true", "on", "1"].contains( - &env::var("CPP_LINTER_PR_REVIEW_SUMMARY_ONLY") - .unwrap_or("false".to_string()) - .as_str(), - ); - - let mut review_comments = ReviewComments::default(); - for file in files { - let file = file.lock().unwrap(); - file.make_suggestions_from_patch(&mut review_comments, summary_only)?; - } - let has_no_changes = - review_comments.full_patch[0].is_empty() && review_comments.full_patch[1].is_empty(); - if has_no_changes && feedback_input.no_lgtm { - log::debug!("Not posting an approved review because `no-lgtm` is true"); - return dismissal.await; - } - let mut payload = FullReview { - event: if feedback_input.passive_reviews { - String::from("COMMENT") - } else if has_no_changes && review_comments.comments.is_empty() { - // if patches have no changes AND there are no comments about clang-tidy diagnostics - String::from("APPROVE") - } else { - String::from("REQUEST_CHANGES") - }, - body: String::new(), - comments: vec![], - }; - payload.body = review_comments.summarize(clang_versions); - if !summary_only { - payload.comments = { - let mut comments = vec![]; - for comment in review_comments.comments { - comments.push(ReviewDiffComment::from(comment)); - } - comments - }; - } - dismissal.await?; // free up the `url` variable - let request = Self::make_api_request( - &self.client, - url, - Method::POST, - Some( - serde_json::to_string(&payload) - .with_context(|| "Failed to serialize PR review to json string")?, - ), - None, - )?; - match send_api_request(&self.client, request, &self.rate_limit_headers).await { - Ok(response) => { - if !response.status().is_success() { - Self::log_response(response, "Failed to post a new PR review").await; - } - } - Err(e) => { - log::error!("Failed to post a new PR review: {e:?}"); - } - } - Ok(()) - } - - /// Dismiss any outdated reviews generated by cpp-linter. - async fn dismiss_outdated_reviews(&self, url: &Url) -> Result<()> { - let mut url_ = Some(Url::parse_with_params(url.as_str(), [("page", "1")])?); - while let Some(ref endpoint) = url_ { - let request = - Self::make_api_request(&self.client, endpoint.as_str(), Method::GET, None, None)?; - let result = send_api_request(&self.client, request, &self.rate_limit_headers).await; - match result { - Err(e) => { - log::error!("Failed to get a list of existing PR reviews: {e:?}"); - return Ok(()); - } - Ok(response) => { - if !response.status().is_success() { - Self::log_response(response, "Failed to get a list of existing PR reviews") - .await; - return Ok(()); - } - url_ = Self::try_next_page(response.headers()); - match serde_json::from_str::>(&response.text().await?) { - Err(e) => { - log::error!("Unable to serialize JSON about review comments: {e:?}"); - return Ok(()); - } - Ok(payload) => { - for review in payload { - if let Some(body) = &review.body - && body.starts_with(COMMENT_MARKER) - && !(["PENDING", "DISMISSED"].contains(&review.state.as_str())) - { - // dismiss outdated review - if let Ok(dismiss_url) = url - .join(format!("reviews/{}/dismissals", review.id).as_str()) - && let Ok(req) = Self::make_api_request( - &self.client, - dismiss_url, - Method::PUT, - Some(REVIEW_DISMISSAL.to_string()), - None, - ) - { - match send_api_request( - &self.client, - req, - &self.rate_limit_headers, - ) - .await - { - Ok(result) => { - if !result.status().is_success() { - Self::log_response( - result, - "Failed to dismiss outdated review", - ) - .await; - } - } - Err(e) => { - log::error!( - "Failed to dismiss outdated review: {e:}" - ); - } - } - } - } - } - } - } - } - } - } - Ok(()) - } -} diff --git a/cpp-linter/src/rest_api/mod.rs b/cpp-linter/src/rest_api/mod.rs deleted file mode 100644 index 9f1634e1..00000000 --- a/cpp-linter/src/rest_api/mod.rs +++ /dev/null @@ -1,662 +0,0 @@ -//! This module is the home of functionality that uses the REST API of various git-based -//! servers. -//! -//! Currently, only Github is supported. - -use std::fmt::{Debug, Display}; -use std::future::Future; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - -// non-std crates -use anyhow::{Error, Result, anyhow}; -use chrono::DateTime; -use reqwest::header::{HeaderMap, HeaderValue}; -use reqwest::{Client, IntoUrl, Method, Request, Response, Url}; - -// project specific modules -pub mod github; -use crate::clang_tools::ClangVersions; -use crate::cli::{FeedbackInput, LinesChangedOnly}; -use crate::common_fs::{FileFilter, FileObj}; - -/// The comment marker used to identify bot comments from other comments (from users or other bots). -pub static COMMENT_MARKER: &str = "\n"; - -/// The user outreach message displayed in bot comments. -pub static USER_OUTREACH: &str = concat!( - "\n\nHave any feedback or feature suggestions? [Share it here.]", - "(https://github.com/cpp-linter/cpp-linter-action/issues)" -); - -/// The user agent string used for HTTP requests. -pub static USER_AGENT: &str = concat!("cpp-linter/", env!("CARGO_PKG_VERSION")); - -/// A structure to contain the different forms of headers that -/// describe a REST API's rate limit status. -#[derive(Debug, Clone)] -pub struct RestApiRateLimitHeaders { - /// The header key of the rate limit's reset time. - pub reset: String, - /// The header key of the rate limit's remaining attempts. - pub remaining: String, - /// The header key of the rate limit's "backoff" time interval. - pub retry: String, -} - -/// A custom trait that templates necessary functionality with a Git server's REST API. -pub trait RestApiClient { - /// A way to set output variables specific to cpp_linter executions in CI. - fn set_exit_code( - &self, - checks_failed: u64, - format_checks_failed: Option, - tidy_checks_failed: Option, - ) -> u64; - - /// This prints a line to indicate the beginning of a related group of log statements. - fn start_log_group(&self, name: String); - - /// This prints a line to indicate the ending of a related group of log statements. - fn end_log_group(&self); - - /// A convenience method to create the headers attached to all REST API calls. - /// - /// If an authentication token is provided (via environment variable), - /// this method shall include the relative information. - fn make_headers() -> Result>; - - /// Construct a HTTP request to be sent. - /// - /// The idea here is that this method is called before [`send_api_request()`]. - /// ```ignore - /// let request = Self::make_api_request( - /// &self.client, - /// "https://example.com", - /// Method::GET, - /// None, - /// None - /// ); - /// let response = send_api_request(&self.client, request, &self.rest_api_headers); - /// match response.await { - /// Ok(res) => {/* handle response */} - /// Err(e) => {/* handle failure */} - /// } - /// ``` - fn make_api_request( - client: &Client, - url: impl IntoUrl, - method: Method, - data: Option, - headers: Option, - ) -> Result { - let mut req = client.request(method, url); - if let Some(h) = headers { - req = req.headers(h); - } - if let Some(d) = data { - req = req.body(d); - } - // RequestBuilder only fails to `build()` if there is a malformed `url`. We - // should be safe here because of this function's `url` parameter type. - req.build().map_err(Error::from) - } - - /// A way to get the list of changed files using REST API calls. It is this method's - /// job to parse diff blobs and return a list of changed files. - /// - /// The context of the file changes are subject to the type of event in which - /// cpp_linter package is used. - /// - /// See [`get_diff()`](crate::git::get_diff()) for explanation of - /// `diff_base` and `ignore_index` parameters, which only applies to a - /// local (non-CI) environment. - fn get_list_of_changed_files( - &self, - file_filter: &FileFilter, - lines_changed_only: &LinesChangedOnly, - diff_base: &Option, - ignore_index: bool, - ) -> impl Future>>; - - /// Makes a comment in MarkDown syntax based on the concerns in `format_advice` and - /// `tidy_advice` about the given set of `files`. - /// - /// This method has a default definition and should not need to be redefined by - /// implementors. - /// - /// Returns the markdown comment as a string as well as the total count of - /// `format_checks_failed` and `tidy_checks_failed` (in respective order). - fn make_comment( - files: &[Arc>], - format_checks_failed: u64, - tidy_checks_failed: u64, - clang_versions: &ClangVersions, - max_len: Option, - ) -> String { - let mut comment = format!("{COMMENT_MARKER}# Cpp-Linter Report "); - let mut remaining_length = - max_len.unwrap_or(u64::MAX) - comment.len() as u64 - USER_OUTREACH.len() as u64; - - if format_checks_failed > 0 || tidy_checks_failed > 0 { - let prompt = ":warning:\nSome files did not pass the configured checks!\n"; - remaining_length -= prompt.len() as u64; - comment.push_str(prompt); - if format_checks_failed > 0 { - make_format_comment( - files, - &mut comment, - format_checks_failed, - // tidy_version should be `Some()` value at this point. - &clang_versions.tidy_version.as_ref().unwrap().to_string(), - &mut remaining_length, - ); - } - if tidy_checks_failed > 0 { - make_tidy_comment( - files, - &mut comment, - tidy_checks_failed, - // format_version should be `Some()` value at this point. - &clang_versions.format_version.as_ref().unwrap().to_string(), - &mut remaining_length, - ); - } - } else { - comment.push_str(":heavy_check_mark:\nNo problems need attention."); - } - comment.push_str(USER_OUTREACH); - comment - } - - /// A way to post feedback in the form of `thread_comments`, `file_annotations`, and - /// `step_summary`. - /// - /// The given `files` should've been gathered from `get_list_of_changed_files()` or - /// `list_source_files()`. - /// - /// The `format_advice` and `tidy_advice` should be a result of parsing output from - /// clang-format and clang-tidy (see `capture_clang_tools_output()`). - /// - /// All other parameters correspond to CLI arguments. - fn post_feedback( - &self, - files: &[Arc>], - user_inputs: FeedbackInput, - clang_versions: ClangVersions, - ) -> impl Future>; - - /// Gets the URL for the next page in a paginated response. - /// - /// Returns [`None`] if current response is the last page. - fn try_next_page(headers: &HeaderMap) -> Option { - if let Some(links) = headers.get("link") - && let Ok(pg_str) = links.to_str() - { - let pages = pg_str.split(", "); - for page in pages { - if page.ends_with("; rel=\"next\"") { - if let Some(link) = page.split_once(">;") { - let url = link.0.trim_start_matches("<").to_string(); - if let Ok(next) = Url::parse(&url) { - return Some(next); - } else { - log::debug!("Failed to parse next page link from response header"); - } - } else { - log::debug!("Response header link for pagination is malformed"); - } - } - } - } - None - } - - fn log_response(response: Response, context: &str) -> impl Future + Send { - async move { - if let Err(e) = response.error_for_status_ref() { - log::error!("{}: {e:?}", context.to_owned()); - if let Ok(text) = response.text().await { - log::error!("{text}"); - } - } - } - } -} - -const MAX_RETRIES: u8 = 5; - -/// A convenience function to send HTTP requests and respect a REST API rate limits. -/// -/// This method respects both primary and secondary rate limits. -/// In the event where the secondary rate limits is reached, -/// this function will wait for a time interval specified the server and retry afterward. -pub async fn send_api_request( - client: &Client, - request: Request, - rate_limit_headers: &RestApiRateLimitHeaders, -) -> Result { - for i in 0..MAX_RETRIES { - let result = client - .execute(request.try_clone().ok_or(anyhow!( - "Failed to clone request object for recursive behavior" - ))?) - .await; - if let Ok(response) = &result { - if [403u16, 429u16].contains(&response.status().as_u16()) { - // rate limit may have been exceeded - - // check if primary rate limit was violated; panic if so. - let mut requests_remaining = None; - if let Some(remaining) = response.headers().get(&rate_limit_headers.remaining) { - if let Ok(count) = remaining.to_str() { - if let Ok(value) = count.parse::() { - requests_remaining = Some(value); - } else { - log::debug!( - "Failed to parse i64 from remaining attempts about rate limit: {count}" - ); - } - } - } else { - // NOTE: I guess it is sometimes valid for a request to - // not include remaining rate limit attempts - log::debug!("Response headers do not include remaining API usage count"); - } - if requests_remaining.is_some_and(|v| v <= 0) { - if let Some(reset_value) = response.headers().get(&rate_limit_headers.reset) { - if let Ok(epoch) = reset_value.to_str() { - if let Ok(value) = epoch.parse::() { - if let Some(reset) = DateTime::from_timestamp(value, 0) { - return Err(anyhow!( - "REST API rate limit exceeded! Resets at {}", - reset - )); - } - } else { - log::debug!( - "Failed to parse i64 from reset time about rate limit: {epoch}" - ); - } - } - } else { - log::debug!("Response headers does not include a reset timestamp"); - } - return Err(anyhow!("REST API rate limit exceeded!")); - } - - // check if secondary rate limit is violated; backoff and try again. - if let Some(retry_value) = response.headers().get(&rate_limit_headers.retry) { - if let Ok(retry_str) = retry_value.to_str() { - if let Ok(retry) = retry_str.parse::() { - let interval = Duration::from_secs(retry + (i as u64).pow(2)); - tokio::time::sleep(interval).await; - } else { - log::debug!( - "Failed to parse u64 from retry interval about rate limit: {retry_str}" - ); - } - } - continue; - } - } - return result.map_err(Error::from); - } - return result.map_err(Error::from); - } - Err(anyhow!( - "REST API secondary rate limit exceeded after {MAX_RETRIES} retries." - )) -} - -fn make_format_comment( - files: &[Arc>], - comment: &mut String, - format_checks_failed: u64, - version_used: &String, - remaining_length: &mut u64, -) { - let opener = format!( - "\n
clang-format (v{version_used}) reports: {format_checks_failed} file(s) not formatted\n\n", - ); - let closer = String::from("\n
"); - let mut format_comment = String::new(); - *remaining_length -= opener.len() as u64 + closer.len() as u64; - for file in files { - let file = file.lock().unwrap(); - if let Some(format_advice) = &file.format_advice - && !format_advice.replacements.is_empty() - && *remaining_length > 0 - { - let note = format!("- {}\n", file.name.to_string_lossy().replace('\\', "/")); - if (note.len() as u64) < *remaining_length { - format_comment.push_str(¬e.to_string()); - *remaining_length -= note.len() as u64; - } - } - } - comment.push_str(&opener); - comment.push_str(&format_comment); - comment.push_str(&closer); -} - -fn make_tidy_comment( - files: &[Arc>], - comment: &mut String, - tidy_checks_failed: u64, - version_used: &String, - remaining_length: &mut u64, -) { - let opener = format!( - "\n
clang-tidy (v{version_used}) reports: {tidy_checks_failed} concern(s)\n\n" - ); - let closer = String::from("\n
"); - let mut tidy_comment = String::new(); - *remaining_length -= opener.len() as u64 + closer.len() as u64; - for file in files { - let file = file.lock().unwrap(); - if let Some(tidy_advice) = &file.tidy_advice { - for tidy_note in &tidy_advice.notes { - let file_path = PathBuf::from(&tidy_note.filename); - if file_path == file.name { - let mut tmp_note = format!("- {}\n\n", tidy_note.filename); - tmp_note.push_str(&format!( - " {filename}:{line}:{cols}: {severity}: [{diagnostic}]\n > {rationale}\n{concerned_code}", - filename = tidy_note.filename, - line = tidy_note.line, - cols = tidy_note.cols, - severity = tidy_note.severity, - diagnostic = tidy_note.diagnostic_link(), - rationale = tidy_note.rationale, - concerned_code = if tidy_note.suggestion.is_empty() {String::from("")} else { - format!("\n ```{ext}\n {suggestion}\n ```\n", - ext = file_path.extension().unwrap_or_default().to_string_lossy(), - suggestion = tidy_note.suggestion.join("\n "), - ).to_string() - }, - ).to_string()); - - if (tmp_note.len() as u64) < *remaining_length { - tidy_comment.push_str(&tmp_note); - *remaining_length -= tmp_note.len() as u64; - } - } - } - } - } - comment.push_str(&opener); - comment.push_str(&tidy_comment); - comment.push_str(&closer); -} - -/// This module tests the silent errors' debug logs -/// from `try_next_page()` and `send_api_request()` functions. -#[cfg(test)] -mod test { - use std::fmt::Display; - use std::sync::{Arc, Mutex}; - - use anyhow::{Result, anyhow}; - use chrono::Utc; - use mockito::{Matcher, Server}; - use reqwest::Method; - use reqwest::{ - Client, - header::{HeaderMap, HeaderValue}, - }; - - use crate::cli::LinesChangedOnly; - use crate::{ - clang_tools::ClangVersions, - cli::FeedbackInput, - common_fs::{FileFilter, FileObj}, - logger, - }; - - use super::{RestApiClient, RestApiRateLimitHeaders, send_api_request}; - - /// A dummy struct to impl RestApiClient - #[derive(Default)] - struct TestClient {} - - impl RestApiClient for TestClient { - fn set_exit_code( - &self, - _checks_failed: u64, - _format_checks_failed: Option, - _tidy_checks_failed: Option, - ) -> u64 { - 0 - } - - fn make_headers() -> Result> { - Err(anyhow!("Not implemented")) - } - - async fn get_list_of_changed_files( - &self, - _file_filter: &FileFilter, - _lines_changed_only: &LinesChangedOnly, - _diff_base: &Option, - _ignore_index: bool, - ) -> Result> { - Err(anyhow!("Not implemented")) - } - - async fn post_feedback( - &self, - _files: &[Arc>], - _user_inputs: FeedbackInput, - _clang_versions: ClangVersions, - ) -> Result { - Err(anyhow!("Not implemented")) - } - - fn start_log_group(&self, name: String) { - log::info!(target: "CI_LOG_GROUPING", "start_log_group: {name}"); - } - - fn end_log_group(&self) { - log::info!(target: "CI_LOG_GROUPING", "end_log_group"); - } - } - - #[tokio::test] - async fn dummy_coverage() { - assert!(TestClient::make_headers().is_err()); - let dummy = TestClient::default(); - dummy.start_log_group("Dummy test".to_string()); - assert_eq!(dummy.set_exit_code(1, None, None), 0); - assert!( - dummy - .get_list_of_changed_files( - &FileFilter::new(&[], vec![]), - &LinesChangedOnly::Off, - &None::, - false - ) - .await - .is_err() - ); - assert!( - dummy - .post_feedback(&[], FeedbackInput::default(), ClangVersions::default()) - .await - .is_err() - ); - dummy.end_log_group(); - } - - // ************************************************* try_next_page() tests - - #[test] - fn bad_link_header() { - let mut headers = HeaderMap::with_capacity(1); - assert!( - headers - .insert("link", HeaderValue::from_str("; rel=\"next\"").unwrap()) - .is_none() - ); - logger::try_init(); - log::set_max_level(log::LevelFilter::Debug); - let result = TestClient::try_next_page(&headers); - assert!(result.is_none()); - } - - #[test] - fn bad_link_domain() { - let mut headers = HeaderMap::with_capacity(1); - assert!( - headers - .insert( - "link", - HeaderValue::from_str("; rel=\"next\"").unwrap() - ) - .is_none() - ); - logger::try_init(); - log::set_max_level(log::LevelFilter::Debug); - let result = TestClient::try_next_page(&headers); - assert!(result.is_none()); - } - - // ************************************************* Rate Limit Tests - - #[derive(Default)] - struct RateLimitTestParams { - secondary: bool, - has_remaining_count: bool, - bad_remaining_count: bool, - has_reset_timestamp: bool, - bad_reset_timestamp: bool, - has_retry_interval: bool, - bad_retry_interval: bool, - } - - async fn simulate_rate_limit(test_params: &RateLimitTestParams) { - let rate_limit_headers = RestApiRateLimitHeaders { - reset: "reset".to_string(), - remaining: "remaining".to_string(), - retry: "retry".to_string(), - }; - logger::try_init(); - log::set_max_level(log::LevelFilter::Debug); - - let mut server = Server::new_async().await; - let client = Client::new(); - let reset_timestamp = (Utc::now().timestamp() + 60).to_string(); - let mut mock = server - .mock("GET", "/") - .match_body(Matcher::Any) - .expect_at_least(1) - .expect_at_most(5) - .with_status(429); - if test_params.has_remaining_count { - mock = mock.with_header( - &rate_limit_headers.remaining, - if test_params.secondary { - "1" - } else if test_params.bad_remaining_count { - "X" - } else { - "0" - }, - ); - } - if test_params.has_reset_timestamp { - mock = mock.with_header( - &rate_limit_headers.reset, - if test_params.bad_reset_timestamp { - "X" - } else { - &reset_timestamp - }, - ); - } - if test_params.secondary && test_params.has_retry_interval { - mock.with_header( - &rate_limit_headers.retry, - if test_params.bad_retry_interval { - "X" - } else { - "0" - }, - ) - .create(); - } else { - mock.create(); - } - let request = - TestClient::make_api_request(&client, server.url(), Method::GET, None, None).unwrap(); - send_api_request(&client, request, &rate_limit_headers) - .await - .unwrap(); - } - - #[tokio::test] - #[should_panic(expected = "REST API secondary rate limit exceeded")] - async fn rate_limit_secondary() { - simulate_rate_limit(&RateLimitTestParams { - secondary: true, - has_retry_interval: true, - ..Default::default() - }) - .await; - } - - #[tokio::test] - #[should_panic(expected = "REST API secondary rate limit exceeded")] - async fn rate_limit_bad_retry() { - simulate_rate_limit(&RateLimitTestParams { - secondary: true, - has_retry_interval: true, - bad_retry_interval: true, - ..Default::default() - }) - .await; - } - - #[tokio::test] - #[should_panic(expected = "REST API rate limit exceeded!")] - async fn rate_limit_primary() { - simulate_rate_limit(&RateLimitTestParams { - has_remaining_count: true, - has_reset_timestamp: true, - ..Default::default() - }) - .await; - } - - #[tokio::test] - #[should_panic(expected = "REST API rate limit exceeded!")] - async fn rate_limit_no_reset() { - simulate_rate_limit(&RateLimitTestParams { - has_remaining_count: true, - ..Default::default() - }) - .await; - } - - #[tokio::test] - #[should_panic(expected = "REST API rate limit exceeded!")] - async fn rate_limit_bad_reset() { - simulate_rate_limit(&RateLimitTestParams { - has_remaining_count: true, - has_reset_timestamp: true, - bad_reset_timestamp: true, - ..Default::default() - }) - .await; - } - - #[tokio::test] - async fn rate_limit_bad_count() { - simulate_rate_limit(&RateLimitTestParams { - has_remaining_count: true, - bad_remaining_count: true, - ..Default::default() - }) - .await; - } -} diff --git a/cpp-linter/src/rest_client/mod.rs b/cpp-linter/src/rest_client/mod.rs new file mode 100644 index 00000000..dc1f5c18 --- /dev/null +++ b/cpp-linter/src/rest_client/mod.rs @@ -0,0 +1,570 @@ +use std::{ + env, + path::PathBuf, + sync::{Arc, Mutex}, +}; + +use git_bot_feedback::{ + AnnotationLevel, CommentKind, CommentPolicy, FileAnnotation, FileFilter, LinesChangedOnly, + OutputVariable, RestApiClient, ReviewAction, ReviewOptions, ThreadCommentOptions, + client::init_client, +}; + +use crate::{ + clang_tools::{ + ClangVersions, ReviewComments, + clang_format::{summarize_style, tally_format_advice}, + clang_tidy::tally_tidy_advice, + }, + cli::{FeedbackInput, ThreadComments}, + common_fs::FileObj, + error::ClientError, +}; + +/// The comment marker used to identify bot comments from other comments (from users or other bots). +pub const COMMENT_MARKER: &str = "\n"; + +/// The UserAgent header value used in HTTP requests. +pub const USER_AGENT: &str = concat!("cpp-linter/", env!("CARGO_PKG_VERSION"),); + +/// The user outreach message displayed in bot comments. +pub const USER_OUTREACH: &str = concat!( + "\n\nHave any feedback or feature suggestions? [Share it here.]", + "(https://github.com/cpp-linter/cpp-linter-action/issues)" +); + +pub struct RestClient { + client: Box, +} + +impl RestClient { + pub fn new() -> Result { + let mut client = init_client()?; + client.set_user_agent(USER_AGENT)?; + Ok(Self { client }) + } + + pub fn is_pr(&self) -> bool { + self.client.is_pr_event() + } + + pub async fn get_list_of_changed_files( + &self, + file_filter: &FileFilter, + lines_changed_only: &LinesChangedOnly, + base_diff: &Option, + ignore_index: bool, + ) -> Result, ClientError> { + let files = self + .client + .get_list_of_changed_files( + file_filter, + lines_changed_only, + base_diff.to_owned(), + ignore_index, + ) + .await?; + Ok(files + .iter() + .map(|(file_name, diff_lines)| { + let diff_chunks = diff_lines + .diff_hunks + .iter() + .map(|hunk| hunk.start..=hunk.end) + .collect(); + FileObj::from( + PathBuf::from(&file_name), + diff_lines.added_lines.clone(), + diff_chunks, + ) + }) + .collect()) + } + + pub fn start_log_group(&self, name: &str) { + self.client.start_log_group(name) + } + + pub fn end_log_group(&self, name: &str) { + self.client.end_log_group(name) + } + + pub async fn post_feedback( + &mut self, + files: &[Arc>], + feedback_inputs: FeedbackInput, + clang_versions: ClangVersions, + ) -> Result { + let tidy_checks_failed = tally_tidy_advice(files).map_err(ClientError::MutexPoisoned)?; + let format_checks_failed = + tally_format_advice(files).map_err(ClientError::MutexPoisoned)?; + let mut comment = None; + + if feedback_inputs.file_annotations { + let annotations = Self::make_annotations(files, &feedback_inputs.style)?; + self.client.write_file_annotations(&annotations)?; + } + if feedback_inputs.step_summary { + comment = Some(Self::make_comment( + files, + format_checks_failed, + tidy_checks_failed, + &clang_versions, + None, + )); + self.client.append_step_summary(comment.as_ref().unwrap())?; + } + let output_vars = [ + OutputVariable { + name: "checks-failed".to_string(), + value: format!("{}", format_checks_failed + tidy_checks_failed), + }, + OutputVariable { + name: "format-checks-failed".to_string(), + value: format_checks_failed.to_string(), + }, + OutputVariable { + name: "tidy-checks-failed".to_string(), + value: tidy_checks_failed.to_string(), + }, + ]; + self.client.write_output_variables(&output_vars)?; + + if feedback_inputs.thread_comments != ThreadComments::Off { + // post thread comment for PR or push event + if comment.as_ref().is_none_or(|c| c.len() > 65535) { + comment = Some(Self::make_comment( + files, + format_checks_failed, + tidy_checks_failed, + &clang_versions, + Some(65535), + )); + } + let options = ThreadCommentOptions { + policy: match feedback_inputs.thread_comments { + ThreadComments::Update => CommentPolicy::Update, + ThreadComments::On => CommentPolicy::Anew, + ThreadComments::Off => unreachable!(), + }, + comment: comment.unwrap_or_default(), + kind: if format_checks_failed == 0 && tidy_checks_failed == 0 { + CommentKind::Lgtm + } else { + CommentKind::Concerns + }, + marker: COMMENT_MARKER.to_string(), + no_lgtm: feedback_inputs.no_lgtm, + }; + self.client.post_thread_comment(options).await?; + } + if self.client.is_pr_event() + && (feedback_inputs.tidy_review || feedback_inputs.format_review) + { + let summary_only = ["true", "on", "1"].contains( + &env::var("CPP_LINTER_PR_REVIEW_SUMMARY_ONLY") + .unwrap_or("false".to_string()) + .as_str(), + ); + let mut review_comments = ReviewComments::default(); + for file in files { + let file = file + .lock() + .map_err(|e| ClientError::MutexPoisoned(e.to_string()))?; + file.make_suggestions_from_patch(&mut review_comments, summary_only)?; + } + + let mut options = ReviewOptions { + marker: COMMENT_MARKER.to_string(), + comments: { + let mut comments = vec![]; + for suggestion in &review_comments.comments { + comments.push(suggestion.as_review_comment()); + } + comments + }, + ..Default::default() + }; + + self.client.cull_pr_reviews(&mut options).await?; + let has_changes = review_comments.full_patch.iter().any(|p| !p.is_empty()); + options.action = if feedback_inputs.passive_reviews { + ReviewAction::Comment + } else { + if options.comments.is_empty() && !has_changes { + ReviewAction::Approve + } else { + ReviewAction::RequestChanges + } + }; + options.summary = review_comments.summarize(&clang_versions, &options.comments); + self.client.post_pr_review(&options).await?; + } + Ok(format_checks_failed + tidy_checks_failed) + } + + /// Post file annotations. + pub fn make_annotations( + files: &[Arc>], + style: &str, + ) -> Result, ClientError> { + let style_guide = summarize_style(style); + let mut annotations = vec![]; + + // iterate over clang-format advice and post annotations + for file in files { + let file = file + .lock() + .map_err(|e| ClientError::MutexPoisoned(e.to_string()))?; + if let Some(format_advice) = &file.format_advice { + // assemble a list of line numbers + let mut lines = Vec::new(); + for replacement in &format_advice.replacements { + if !lines.contains(&replacement.line) { + lines.push(replacement.line); + } + } + // post annotation if any applicable lines were formatted + if !lines.is_empty() { + let name = file.name.to_string_lossy().replace('\\', "/"); + let title = format!("Run clang-format on {name}"); + let message = format!( + "File {name} does not conform to {style_guide} style guidelines. (lines {line_set})", + line_set = lines + .iter() + .map(|val| val.to_string()) + .collect::>() + .join(","), + ); + let annotation = FileAnnotation { + severity: AnnotationLevel::Notice, + path: name, + start_line: None, + end_line: None, + start_column: None, + end_column: None, + title: Some(title), + message, + }; + annotations.push(annotation); + } + } // end format_advice iterations + + // iterate over clang-tidy advice and post annotations + // The tidy_advice vector is parallel to the files vector; meaning it serves as a file filterer. + // lines are already filter as specified to clang-tidy CLI. + if let Some(tidy_advice) = &file.tidy_advice { + for note in &tidy_advice.notes { + let path = file.name.to_string_lossy().replace('\\', "/"); + if note.filename == path { + let title = format!("{}:{}:{}", note.filename, note.line, note.cols); + let annotation = FileAnnotation { + severity: match note.severity.as_str() { + "note" => AnnotationLevel::Notice, + "warning" => AnnotationLevel::Warning, + "error" => AnnotationLevel::Error, + _ => AnnotationLevel::Notice, // default to notice if severity is unrecognized + }, + path, + start_line: None, + end_line: Some(note.line as usize), + start_column: None, + end_column: Some(note.cols as usize), + title: Some(title), + message: note.rationale.clone(), + }; + annotations.push(annotation); + } + } + } + } + Ok(annotations) + } + + /// Makes a comment in MarkDown syntax based on the concerns in `format_advice` and + /// `tidy_advice` about the given set of `files`. + /// + /// This method has a default definition and should not need to be redefined by + /// implementors. + /// + /// Returns the markdown comment as a string as well as the total count of + /// `format_checks_failed` and `tidy_checks_failed` (in respective order). + fn make_comment( + files: &[Arc>], + format_checks_failed: u64, + tidy_checks_failed: u64, + clang_versions: &ClangVersions, + max_len: Option, + ) -> String { + let mut comment = format!("{COMMENT_MARKER}# Cpp-Linter Report "); + let mut remaining_length = + max_len.unwrap_or(u64::MAX) - comment.len() as u64 - USER_OUTREACH.len() as u64; + + if format_checks_failed > 0 || tidy_checks_failed > 0 { + let prompt = ":warning:\nSome files did not pass the configured checks!\n"; + remaining_length -= prompt.len() as u64; + comment.push_str(prompt); + if format_checks_failed > 0 { + make_format_comment( + files, + &mut comment, + format_checks_failed, + // tidy_version should be `Some()` value at this point. + &clang_versions.tidy_version.as_ref().unwrap().to_string(), + &mut remaining_length, + ); + } + if tidy_checks_failed > 0 { + make_tidy_comment( + files, + &mut comment, + tidy_checks_failed, + // format_version should be `Some()` value at this point. + &clang_versions.format_version.as_ref().unwrap().to_string(), + &mut remaining_length, + ); + } + } else { + comment.push_str(":heavy_check_mark:\nNo problems need attention."); + } + comment.push_str(USER_OUTREACH); + comment + } +} + +fn make_format_comment( + files: &[Arc>], + comment: &mut String, + format_checks_failed: u64, + version_used: &String, + remaining_length: &mut u64, +) { + let opener = format!( + "\n
clang-format (v{version_used}) reports: {format_checks_failed} file(s) not formatted\n\n", + ); + let closer = String::from("\n
"); + let mut format_comment = String::new(); + *remaining_length -= opener.len() as u64 + closer.len() as u64; + for file in files { + let file = file.lock().unwrap(); + if let Some(format_advice) = &file.format_advice + && !format_advice.replacements.is_empty() + && *remaining_length > 0 + { + let note = format!("- {}\n", file.name.to_string_lossy().replace('\\', "/")); + if (note.len() as u64) < *remaining_length { + format_comment.push_str(¬e.to_string()); + *remaining_length -= note.len() as u64; + } + } + } + comment.push_str(&opener); + comment.push_str(&format_comment); + comment.push_str(&closer); +} + +fn make_tidy_comment( + files: &[Arc>], + comment: &mut String, + tidy_checks_failed: u64, + version_used: &String, + remaining_length: &mut u64, +) { + let opener = format!( + "\n
clang-tidy (v{version_used}) reports: {tidy_checks_failed} concern(s)\n\n" + ); + let closer = String::from("\n
"); + let mut tidy_comment = String::new(); + *remaining_length -= opener.len() as u64 + closer.len() as u64; + for file in files { + let file = file.lock().unwrap(); + if let Some(tidy_advice) = &file.tidy_advice { + for tidy_note in &tidy_advice.notes { + let file_path = PathBuf::from(&tidy_note.filename); + if file_path == file.name { + let mut tmp_note = format!("- {}\n\n", tidy_note.filename); + tmp_note.push_str(&format!( + " {filename}:{line}:{cols}: {severity}: [{diagnostic}]\n > {rationale}\n{concerned_code}", + filename = tidy_note.filename, + line = tidy_note.line, + cols = tidy_note.cols, + severity = tidy_note.severity, + diagnostic = tidy_note.diagnostic_link(), + rationale = tidy_note.rationale, + concerned_code = if tidy_note.suggestion.is_empty() {String::from("")} else { + format!("\n ```{ext}\n {suggestion}\n ```\n", + ext = file_path.extension().unwrap_or_default().to_string_lossy(), + suggestion = tidy_note.suggestion.join("\n "), + ).to_string() + }, + ).to_string()); + + if (tmp_note.len() as u64) < *remaining_length { + tidy_comment.push_str(&tmp_note); + *remaining_length -= tmp_note.len() as u64; + } + } + } + } + } + comment.push_str(&opener); + comment.push_str(&tidy_comment); + comment.push_str(&closer); +} + +#[cfg(all(test, feature = "bin"))] +mod test { + use std::{ + default::Default, + env, + io::Read, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, + }; + + use regex::Regex; + use semver::Version; + use tempfile::{NamedTempFile, tempdir}; + + use super::{RestClient, USER_OUTREACH}; + use crate::{ + clang_tools::{ + ClangVersions, + clang_format::{FormatAdvice, Replacement}, + clang_tidy::{TidyAdvice, TidyNotification}, + }, + cli::FeedbackInput, + common_fs::FileObj, + logger, + }; + + // ************************* tests for step-summary and output variables + + async fn create_comment( + is_lgtm: bool, + fail_gh_out: bool, + fail_summary: bool, + ) -> (String, String) { + let tmp_dir = tempdir().unwrap(); + unsafe { + // ensure we are mimicking a CI platform + env::set_var("GITHUB_ACTIONS", "true"); + env::set_var("GITHUB_REPOSITORY", "cpp-linter/cpp-linter-rs"); + env::set_var("GITHUB_SHA", "deadbeef123"); + } + let mut rest_api_client = RestClient::new().unwrap(); + logger::try_init(); + if env::var("ACTIONS_STEP_DEBUG").is_ok_and(|var| var == "true") { + // assert!(rest_api_client.debug_enabled); + log::set_max_level(log::LevelFilter::Debug); + } + let mut files = vec![]; + if !is_lgtm { + for _i in 0..65535 { + let filename = String::from("tests/demo/demo.cpp"); + let mut file = FileObj::new(PathBuf::from(&filename)); + let notes = vec![TidyNotification { + filename, + line: 0, + cols: 0, + severity: String::from("note"), + rationale: String::from("A test dummy rationale"), + diagnostic: String::from("clang-diagnostic-warning"), + suggestion: vec![], + fixed_lines: vec![], + }]; + file.tidy_advice = Some(TidyAdvice { + notes, + patched: None, + }); + file.format_advice = Some(FormatAdvice { + replacements: vec![Replacement { offset: 0, line: 1 }], + patched: None, + }); + files.push(Arc::new(Mutex::new(file))); + } + } + let feedback_inputs = FeedbackInput { + style: if is_lgtm { + String::new() + } else { + String::from("file") + }, + step_summary: true, + ..Default::default() + }; + let mut step_summary_path = NamedTempFile::new_in(tmp_dir.path()).unwrap(); + let mut gh_out_path = NamedTempFile::new_in(tmp_dir.path()).unwrap(); + unsafe { + env::set_var( + "GITHUB_STEP_SUMMARY", + if fail_summary { + Path::new("not-a-file.txt") + } else { + step_summary_path.path() + }, + ); + env::set_var( + "GITHUB_OUTPUT", + if fail_gh_out { + Path::new("not-a-file.txt") + } else { + gh_out_path.path() + }, + ); + } + let clang_versions = ClangVersions { + format_version: Some(Version::new(1, 2, 3)), + tidy_version: Some(Version::new(1, 2, 3)), + }; + rest_api_client + .post_feedback(&files, feedback_inputs, clang_versions) + .await + .unwrap(); + let mut step_summary_content = String::new(); + step_summary_path + .read_to_string(&mut step_summary_content) + .unwrap(); + if !fail_summary { + assert!(&step_summary_content.contains(USER_OUTREACH)); + } + let mut gh_out_content = String::new(); + gh_out_path.read_to_string(&mut gh_out_content).unwrap(); + if !fail_gh_out { + assert!(gh_out_content.starts_with("checks-failed=")); + } + (step_summary_content, gh_out_content) + } + + #[tokio::test] + async fn check_comment_concerns() { + let (comment, gh_out) = create_comment(false, false, false).await; + assert!(&comment.contains(":warning:\nSome files did not pass the configured checks!\n")); + let fmt_pattern = Regex::new(r"format-checks-failed=(\d+)\n").unwrap(); + let tidy_pattern = Regex::new(r"tidy-checks-failed=(\d+)\n").unwrap(); + for pattern in [fmt_pattern, tidy_pattern] { + let number = pattern + .captures(&gh_out) + .expect("found no number of checks-failed") + .get(1) + .unwrap() + .as_str() + .parse::() + .unwrap(); + assert!(number > 0); + } + } + + #[tokio::test] + async fn check_comment_lgtm() { + unsafe { + env::set_var("ACTIONS_STEP_DEBUG", "true"); + } + let (comment, gh_out) = create_comment(true, false, false).await; + assert!(comment.contains(":heavy_check_mark:\nNo problems need attention.")); + assert_eq!( + gh_out, + "checks-failed=0\nformat-checks-failed=0\ntidy-checks-failed=0\n" + ); + } +} diff --git a/cpp-linter/src/run.rs b/cpp-linter/src/run.rs index 6229acd5..0726d48d 100644 --- a/cpp-linter/src/run.rs +++ b/cpp-linter/src/run.rs @@ -6,7 +6,7 @@ #![cfg(feature = "bin")] use std::{ env, - path::Path, + path::{Path, PathBuf}, sync::{Arc, Mutex}, }; @@ -20,10 +20,11 @@ use log::{LevelFilter, set_max_level}; use crate::{ clang_tools::capture_clang_tools_output, cli::{ClangParams, Cli, CliCommand, FeedbackInput, LinesChangedOnly}, - common_fs::FileFilter, + common_fs::FileObj, logger, - rest_api::{RestApiClient, github::GithubApiClient}, + rest_client::RestClient, }; +use git_bot_feedback::FileFilter; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -67,20 +68,31 @@ pub async fn run_main(args: Vec) -> Result<()> { })?; } - let rest_api_client = GithubApiClient::new()?; + let mut rest_api_client = RestClient::new()?; set_max_level( - if cli.general_options.verbosity.is_debug() || rest_api_client.debug_enabled { + if cli.general_options.verbosity.is_debug() + /* || rest_api_client.debug_enabled */ + { LevelFilter::Debug } else { LevelFilter::Info }, ); - log::info!("Processing event {}", rest_api_client.event_name); - let is_pr = rest_api_client.event_name == "pull_request"; + // log::info!("Processing event {}", rest_api_client.event_name); + let is_pr = rest_api_client.is_pr(); let mut file_filter = FileFilter::new( - &cli.source_options.ignore, - cli.source_options.extensions.clone(), + &cli.source_options + .ignore + .iter() + .map(|s| s.as_str()) + .collect::>(), + &cli.source_options + .extensions + .iter() + .map(|s| s.as_str()) + .collect::>(), + None, ); file_filter.parse_submodules(); if let Some(files) = &cli.not_ignored { @@ -100,7 +112,7 @@ pub async fn run_main(args: Vec) -> Result<()> { } } - rest_api_client.start_log_group(String::from("Get list of specified source files")); + rest_api_client.start_log_group("Get list of specified source files"); let files = if !matches!(cli.source_options.lines_changed_only, LinesChangedOnly::Off) || cli.source_options.files_changed_only { @@ -108,19 +120,23 @@ pub async fn run_main(args: Vec) -> Result<()> { rest_api_client .get_list_of_changed_files( &file_filter, - &cli.source_options.lines_changed_only, + &cli.source_options.lines_changed_only.clone().into(), &cli.source_options.diff_base, cli.source_options.ignore_index, ) .await? } else { // walk the folder and look for files with specified extensions according to ignore values. - let mut all_files = file_filter.list_source_files(".")?; + let mut all_files: Vec = file_filter + .walk_dir(".")? + .into_iter() + .map(|file_name| FileObj::new(PathBuf::from(&file_name))) + .collect(); if is_pr && (cli.feedback_options.tidy_review || cli.feedback_options.format_review) { let changed_files = rest_api_client .get_list_of_changed_files( &file_filter, - &LinesChangedOnly::Off, + &LinesChangedOnly::Off.into(), &cli.source_options.diff_base, cli.source_options.ignore_index, ) @@ -143,7 +159,7 @@ pub async fn run_main(args: Vec) -> Result<()> { log::info!(" ./{}", file.name.to_string_lossy().replace('\\', "/")); arc_files.push(Arc::new(Mutex::new(file))); } - rest_api_client.end_log_group(); + rest_api_client.end_log_group("Get list of specified source files"); let mut clang_params = ClangParams::from(&cli); clang_params.format_review &= is_pr; @@ -156,11 +172,11 @@ pub async fn run_main(args: Vec) -> Result<()> { &rest_api_client, ) .await?; - rest_api_client.start_log_group(String::from("Posting feedback")); + rest_api_client.start_log_group("Posting feedback"); let checks_failed = rest_api_client .post_feedback(&arc_files, user_inputs, clang_versions) .await?; - rest_api_client.end_log_group(); + rest_api_client.end_log_group("Posting feedback"); if env::var("PRE_COMMIT").is_ok_and(|v| v == "1") && checks_failed > 1 { return Err(anyhow!("Some checks did not pass")); } diff --git a/cpp-linter/tests/.hidden/test_asset.txt b/cpp-linter/tests/.hidden/test_asset.txt deleted file mode 100644 index 83788f1a..00000000 --- a/cpp-linter/tests/.hidden/test_asset.txt +++ /dev/null @@ -1 +0,0 @@ -This file is here for completeness when testing file filters. diff --git a/cpp-linter/tests/comment_test_assets/patch.diff b/cpp-linter/tests/comment_test_assets/patch.diff deleted file mode 100644 index 3c5dd0b5..00000000 --- a/cpp-linter/tests/comment_test_assets/patch.diff +++ /dev/null @@ -1,108 +0,0 @@ -diff --git a/.github/workflows/cpp-lint-package.yml b/.github/workflows/cpp-lint-package.yml -index 0418957..3b8c454 100644 ---- a/.github/workflows/cpp-lint-package.yml -+++ b/.github/workflows/cpp-lint-package.yml -@@ -7,6 +7,7 @@ on: - description: 'which branch to test' - default: 'main' - required: true -+ pull_request: - - jobs: - cpp-linter: -@@ -14,9 +15,9 @@ jobs: - - strategy: - matrix: -- clang-version: ['7', '8', '9','10', '11', '12', '13', '14', '15', '16', '17'] -+ clang-version: ['10', '11', '12', '13', '14', '15', '16', '17'] - repo: ['cpp-linter/cpp-linter'] -- branch: ['${{ inputs.branch }}'] -+ branch: ['pr-review-suggestions'] - fail-fast: false - - steps: -@@ -62,10 +63,12 @@ jobs: - -i=build - -p=build - -V=${{ runner.temp }}/llvm -- -f=false - --extra-arg="-std=c++14 -Wall" -- --thread-comments=${{ matrix.clang-version == '12' }} -- -a=${{ matrix.clang-version == '12' }} -+ --file-annotations=false -+ --lines-changed-only=true -+ --thread-comments=${{ matrix.clang-version == '16' }} -+ --tidy-review=${{ matrix.clang-version == '16' }} -+ --format-review=${{ matrix.clang-version == '16' }} - - - name: Fail fast?! - if: steps.linter.outputs.checks-failed > 0 -diff --git a/src/demo.cpp b/src/demo.cpp -index 0c1db60..1bf553e 100644 ---- a/src/demo.cpp -+++ b/src/demo.cpp -@@ -1,17 +1,18 @@ - /** This is a very ugly test code (doomed to fail linting) */ - #include "demo.hpp" --#include --#include -+#include - --// using size_t from cstddef --size_t dummyFunc(size_t i) { return i; } - --int main() --{ -- for (;;) -- break; -+ -+ -+int main(){ -+ -+ for (;;) break; -+ - - printf("Hello world!\n"); - -- return 0; --} -+ -+ -+ -+ return 0;} -diff --git a/src/demo.hpp b/src/demo.hpp -index 2695731..f93d012 100644 ---- a/src/demo.hpp -+++ b/src/demo.hpp -@@ -5,12 +5,10 @@ - class Dummy { - char* useless; - int numb; -+ Dummy() :numb(0), useless("\0"){} - - public: -- void *not_usefull(char *str){ -- useless = str; -- return 0; -- } -+ void *not_useful(char *str){useless = str;} - }; - - -@@ -28,14 +26,11 @@ class Dummy { - - - -- -- -- -- - - - struct LongDiff - { -+ - long diff; - - }; diff --git a/cpp-linter/tests/comment_test_assets/pr_comments_pg1.json b/cpp-linter/tests/comment_test_assets/pr_comments_pg1.json index 72b8cfbc..33608181 100644 --- a/cpp-linter/tests/comment_test_assets/pr_comments_pg1.json +++ b/cpp-linter/tests/comment_test_assets/pr_comments_pg1.json @@ -2,7 +2,7 @@ { "url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/issues/comments/1782261434", "html_url": "https://github.com/cpp-linter/test-cpp-linter-action/pull/22#issuecomment-1782261434", - "issue_url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/issues/22", + "issue_url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/issues/42", "id": 1782261434, "node_id": "IC_kwDOFY2uzM5qOya6", "user": { diff --git a/cpp-linter/tests/comment_test_assets/pr_comments_pg2.json b/cpp-linter/tests/comment_test_assets/pr_comments_pg2.json index 72b8cfbc..33608181 100644 --- a/cpp-linter/tests/comment_test_assets/pr_comments_pg2.json +++ b/cpp-linter/tests/comment_test_assets/pr_comments_pg2.json @@ -2,7 +2,7 @@ { "url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/issues/comments/1782261434", "html_url": "https://github.com/cpp-linter/test-cpp-linter-action/pull/22#issuecomment-1782261434", - "issue_url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/issues/22", + "issue_url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/issues/42", "id": 1782261434, "node_id": "IC_kwDOFY2uzM5qOya6", "user": { diff --git a/cpp-linter/tests/comment_test_assets/pr_diff.json b/cpp-linter/tests/comment_test_assets/pr_diff.json new file mode 100644 index 00000000..11538a6b --- /dev/null +++ b/cpp-linter/tests/comment_test_assets/pr_diff.json @@ -0,0 +1,74 @@ +[ + { + "sha": "1dd236cb113f5e05df15f34dfd338b3dd8b164cd", + "filename": ".clang-format", + "status": "modified", + "additions": 1, + "deletions": 1, + "changes": 2, + "blob_url": "https://github.com/cpp-linter/test-cpp-linter-action/blob/01e2ebf010b949986fa3e6d6e81e7f7c8da90001/.clang-format", + "raw_url": "https://github.com/cpp-linter/test-cpp-linter-action/raw/01e2ebf010b949986fa3e6d6e81e7f7c8da90001/.clang-format", + "contents_url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/contents/.clang-format?ref=01e2ebf010b949986fa3e6d6e81e7f7c8da90001", + "patch": "@@ -1,3 +1,3 @@\n ---\n Language: Cpp\n-BasedOnStyle: WebKit\n\\ No newline at end of file\n+BasedOnStyle: WebKit" + }, + { + "sha": "d3865adefcf84328aa3e1015942d8a60680f726a", + "filename": ".clang-tidy", + "status": "modified", + "additions": 1, + "deletions": 0, + "changes": 1, + "blob_url": "https://github.com/cpp-linter/test-cpp-linter-action/blob/01e2ebf010b949986fa3e6d6e81e7f7c8da90001/.clang-tidy", + "raw_url": "https://github.com/cpp-linter/test-cpp-linter-action/raw/01e2ebf010b949986fa3e6d6e81e7f7c8da90001/.clang-tidy", + "contents_url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/contents/.clang-tidy?ref=01e2ebf010b949986fa3e6d6e81e7f7c8da90001", + "patch": "@@ -183,3 +183,4 @@ CheckOptions:\n value: '1'\n - key: readability-uppercase-literal-suffix.NewSuffixes\n value: ''\n+..." + }, + { + "sha": "7c6ca7ac47687ea5caae171ccd04525c08312bc6", + "filename": ".github/workflows/cpp-lint-action.yml", + "status": "modified", + "additions": 7, + "deletions": 7, + "changes": 14, + "blob_url": "https://github.com/cpp-linter/test-cpp-linter-action/blob/01e2ebf010b949986fa3e6d6e81e7f7c8da90001/.github%2Fworkflows%2Fcpp-lint-action.yml", + "raw_url": "https://github.com/cpp-linter/test-cpp-linter-action/raw/01e2ebf010b949986fa3e6d6e81e7f7c8da90001/.github%2Fworkflows%2Fcpp-lint-action.yml", + "contents_url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/contents/.github%2Fworkflows%2Fcpp-lint-action.yml?ref=01e2ebf010b949986fa3e6d6e81e7f7c8da90001", + "patch": "@@ -32,7 +32,7 @@ jobs:\n run: mkdir build && cmake -Bbuild src\n \n - name: Run linter as action\n- uses: cpp-linter/cpp-linter-action@latest\n+ uses: cpp-linter/cpp-linter-action@main\n id: linter\n env:\n GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n@@ -42,17 +42,17 @@ jobs:\n # to ignore all build folder contents\n ignore: build\n database: build\n- verbosity: 9\n+ verbosity: debug\n version: ${{ matrix.clang-version }}\n- thread-comments: ${{ matrix.clang-version == '12' }}\n- file-annotations: ${{ matrix.clang-version == '12' }}\n+ thread-comments: ${{ matrix.clang-version == '16' }}\n+ file-annotations: ${{ matrix.clang-version == '16' }}\n extra-args: -std=c++14 -Wall\n \n - name: Fail fast?!\n # if: steps.linter.outputs.checks-failed > 0\n run: | \n- echo \"some linter checks failed\"\n- echo \"${{ steps.linter.outputs.checks-failed }}\"\n- echo \"${{ env.checks-failed }}\"\n+ echo \"checks-failed: ${{ steps.linter.outputs.checks-failed }}\"\n+ echo \"clang-tidy-checks-failed: ${{ steps.linter.outputs.clang-tidy-checks-failed }}\"\n+ echo \"clang-format-checks-failed: ${{ steps.linter.outputs.clang-format-checks-failed }}\"\n # for actual deployment\n # run: exit 1" + }, + { + "sha": "18b133e7997cdace197d391cc01e9b192b446df0", + "filename": ".github/workflows/cpp-lint-package.yml", + "status": "modified", + "additions": 10, + "deletions": 3, + "changes": 13, + "blob_url": "https://github.com/cpp-linter/test-cpp-linter-action/blob/01e2ebf010b949986fa3e6d6e81e7f7c8da90001/.github%2Fworkflows%2Fcpp-lint-package.yml", + "raw_url": "https://github.com/cpp-linter/test-cpp-linter-action/raw/01e2ebf010b949986fa3e6d6e81e7f7c8da90001/.github%2Fworkflows%2Fcpp-lint-package.yml", + "contents_url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/contents/.github%2Fworkflows%2Fcpp-lint-package.yml?ref=01e2ebf010b949986fa3e6d6e81e7f7c8da90001", + "patch": "@@ -5,8 +5,9 @@ on:\n inputs:\n branch:\n description: 'which branch to test'\n- default: 'main'\n+ default: 'fix-test-coverage'\n required: true\n+ pull_request:\n \n jobs:\n cpp-linter:\n@@ -16,7 +17,8 @@ jobs:\n matrix:\n clang-version: ['7', '8', '9','10', '11', '12', '13', '14', '15', '16', '17']\n repo: ['cpp-linter/cpp-linter']\n- branch: ['${{ inputs.branch }}']\n+ branch:\n+ - ${{ github.event_name == 'workflow_dispatch' && inputs.branch || 'fix-test-coverage' }}\n fail-fast: false\n \n steps:\n@@ -66,9 +68,14 @@ jobs:\n --extra-arg=\"-std=c++14 -Wall\"\n --thread-comments=${{ matrix.clang-version == '17' && 'update' }}\n -a=${{ matrix.clang-version == '17' }}\n+ --tidy-review=${{ matrix.clang-version == '17' }}\n+ --format-review=${{ matrix.clang-version == '17' }}\n \n - name: Fail fast?!\n if: steps.linter.outputs.checks-failed > 0\n- run: echo \"Some files failed the linting checks!\"\n+ run: |\n+ echo \"checks-failed: ${{ steps.linter.outputs.checks-failed }}\"\n+ echo \"clang-tidy-checks-failed: ${{ steps.linter.outputs.clang-tidy-checks-failed }}\"\n+ echo \"clang-format-checks-failed: ${{ steps.linter.outputs.clang-format-checks-failed }}\"\n # for actual deployment\n # run: exit 1" + }, + { + "sha": "1bf553e06e4b7c6c9a9be5da4845acbdeb04f6a5", + "filename": "src/demo.cpp", + "status": "modified", + "additions": 11, + "deletions": 10, + "changes": 21, + "blob_url": "https://github.com/cpp-linter/test-cpp-linter-action/blob/01e2ebf010b949986fa3e6d6e81e7f7c8da90001/src%2Fdemo.cpp", + "raw_url": "https://github.com/cpp-linter/test-cpp-linter-action/raw/01e2ebf010b949986fa3e6d6e81e7f7c8da90001/src%2Fdemo.cpp", + "contents_url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/contents/src%2Fdemo.cpp?ref=01e2ebf010b949986fa3e6d6e81e7f7c8da90001", + "patch": "@@ -1,17 +1,18 @@\n /** This is a very ugly test code (doomed to fail linting) */\n #include \"demo.hpp\"\n-#include \n-#include \n+#include \n \n-// using size_t from cstddef\n-size_t dummyFunc(size_t i) { return i; }\n \n-int main()\n-{\n- for (;;)\n- break;\n+\n+\n+int main(){\n+\n+ for (;;) break;\n+\n \n printf(\"Hello world!\\n\");\n \n- return 0;\n-}\n+\n+\n+\n+ return 0;}" + }, + { + "sha": "f93d0122ae2e3c1952c795837d71c432036b55eb", + "filename": "src/demo.hpp", + "status": "modified", + "additions": 3, + "deletions": 8, + "changes": 11, + "blob_url": "https://github.com/cpp-linter/test-cpp-linter-action/blob/01e2ebf010b949986fa3e6d6e81e7f7c8da90001/src%2Fdemo.hpp", + "raw_url": "https://github.com/cpp-linter/test-cpp-linter-action/raw/01e2ebf010b949986fa3e6d6e81e7f7c8da90001/src%2Fdemo.hpp", + "contents_url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/contents/src%2Fdemo.hpp?ref=01e2ebf010b949986fa3e6d6e81e7f7c8da90001", + "patch": "@@ -5,12 +5,10 @@\n class Dummy {\n char* useless;\n int numb;\n+ Dummy() :numb(0), useless(\"\\0\"){}\n \n public:\n- void *not_usefull(char *str){\n- useless = str;\n- return 0;\n- }\n+ void *not_useful(char *str){useless = str;}\n };\n \n \n@@ -28,14 +26,11 @@ class Dummy {\n \n \n \n-\n-\n-\n-\n \n \n struct LongDiff\n {\n+\n long diff;\n \n };" + } +] diff --git a/cpp-linter/tests/comment_test_assets/push_comments_8d68756375e0483c7ac2b4d6bbbece420dbbb495.json b/cpp-linter/tests/comment_test_assets/push_comments.json similarity index 97% rename from cpp-linter/tests/comment_test_assets/push_comments_8d68756375e0483c7ac2b4d6bbbece420dbbb495.json rename to cpp-linter/tests/comment_test_assets/push_comments.json index 13843925..603d5d3a 100644 --- a/cpp-linter/tests/comment_test_assets/push_comments_8d68756375e0483c7ac2b4d6bbbece420dbbb495.json +++ b/cpp-linter/tests/comment_test_assets/push_comments.json @@ -1,7 +1,7 @@ [ { "url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/comments/76453652", - "html_url": "https://github.com/cpp-linter/test-cpp-linter-action/commit/8d68756375e0483c7ac2b4d6bbbece420dbbb495#commitcomment-76453652", + "html_url": "https://github.com/cpp-linter/test-cpp-linter-action/commit/01e2ebf010b949986fa3e6d6e81e7f7c8da90001#commitcomment-76453652", "id": 76453652, "node_id": "CC_kwDOFY2uzM4EjpcU", "user": { @@ -27,7 +27,7 @@ "position": null, "line": null, "path": null, - "commit_id": "8d68756375e0483c7ac2b4d6bbbece420dbbb495", + "commit_id": "01e2ebf010b949986fa3e6d6e81e7f7c8da90001", "created_at": "2022-06-19T12:17:04Z", "updated_at": "2022-06-19T12:17:04Z", "author_association": "NONE", @@ -47,7 +47,7 @@ }, { "url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/comments/76453652", - "html_url": "https://github.com/cpp-linter/test-cpp-linter-action/commit/8d68756375e0483c7ac2b4d6bbbece420dbbb495#commitcomment-76453652", + "html_url": "https://github.com/cpp-linter/test-cpp-linter-action/commit/01e2ebf010b949986fa3e6d6e81e7f7c8da90001#commitcomment-76453652", "id": 76453653, "node_id": "CC_kwDOFY2uzM4EjpcU", "user": { @@ -73,7 +73,7 @@ "position": null, "line": null, "path": null, - "commit_id": "8d68756375e0483c7ac2b4d6bbbece420dbbb495", + "commit_id": "01e2ebf010b949986fa3e6d6e81e7f7c8da90001", "created_at": "2022-06-19T12:17:04Z", "updated_at": "2022-06-19T12:17:04Z", "author_association": "NONE", @@ -93,7 +93,7 @@ }, { "url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/comments/76453652", - "html_url": "https://github.com/cpp-linter/test-cpp-linter-action/commit/8d68756375e0483c7ac2b4d6bbbece420dbbb495#commitcomment-76453652", + "html_url": "https://github.com/cpp-linter/test-cpp-linter-action/commit/01e2ebf010b949986fa3e6d6e81e7f7c8da90001#commitcomment-76453652", "id": 76453652, "node_id": "CC_kwDOFY2uzM4EjpcU", "user": { @@ -119,7 +119,7 @@ "position": null, "line": null, "path": null, - "commit_id": "8d68756375e0483c7ac2b4d6bbbece420dbbb495", + "commit_id": "01e2ebf010b949986fa3e6d6e81e7f7c8da90001", "created_at": "2022-06-19T12:17:04Z", "updated_at": "2022-06-19T12:17:04Z", "author_association": "NONE", diff --git a/cpp-linter/tests/comment_test_assets/push_diff.json b/cpp-linter/tests/comment_test_assets/push_diff.json new file mode 100644 index 00000000..bb6014cb --- /dev/null +++ b/cpp-linter/tests/comment_test_assets/push_diff.json @@ -0,0 +1,113 @@ +{ + "sha": "01e2ebf010b949986fa3e6d6e81e7f7c8da90001", + "node_id": "C_kwDOFY2uzNoAKDAxZTJlYmYwMTBiOTQ5OTg2ZmEzZTZkNmU4MWU3ZjdjOGRhOTAwMDE", + "commit": { + "author": { + "name": "Brendan", + "email": "2bndy5@gmail.com", + "date": "2024-12-12T16:04:15Z" + }, + "committer": { + "name": "Brendan", + "email": "2bndy5@gmail.com", + "date": "2024-12-12T16:04:15Z" + }, + "message": "make source changes", + "tree": { + "sha": "c2ce8a3ed0d96bf72e5c4e34857349f0e97f3fb4", + "url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/git/trees/c2ce8a3ed0d96bf72e5c4e34857349f0e97f3fb4" + }, + "url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/git/commits/01e2ebf010b949986fa3e6d6e81e7f7c8da90001", + "comment_count": 0, + "verification": { + "verified": false, + "reason": "unsigned", + "signature": null, + "payload": null, + "verified_at": null + } + }, + "url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/commits/01e2ebf010b949986fa3e6d6e81e7f7c8da90001", + "html_url": "https://github.com/cpp-linter/test-cpp-linter-action/commit/01e2ebf010b949986fa3e6d6e81e7f7c8da90001", + "comments_url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/commits/01e2ebf010b949986fa3e6d6e81e7f7c8da90001/comments", + "author": { + "login": "2bndy5", + "id": 14963867, + "node_id": "MDQ6VXNlcjE0OTYzODY3", + "avatar_url": "https://avatars.githubusercontent.com/u/14963867?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/2bndy5", + "html_url": "https://github.com/2bndy5", + "followers_url": "https://api.github.com/users/2bndy5/followers", + "following_url": "https://api.github.com/users/2bndy5/following{/other_user}", + "gists_url": "https://api.github.com/users/2bndy5/gists{/gist_id}", + "starred_url": "https://api.github.com/users/2bndy5/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/2bndy5/subscriptions", + "organizations_url": "https://api.github.com/users/2bndy5/orgs", + "repos_url": "https://api.github.com/users/2bndy5/repos", + "events_url": "https://api.github.com/users/2bndy5/events{/privacy}", + "received_events_url": "https://api.github.com/users/2bndy5/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "committer": { + "login": "2bndy5", + "id": 14963867, + "node_id": "MDQ6VXNlcjE0OTYzODY3", + "avatar_url": "https://avatars.githubusercontent.com/u/14963867?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/2bndy5", + "html_url": "https://github.com/2bndy5", + "followers_url": "https://api.github.com/users/2bndy5/followers", + "following_url": "https://api.github.com/users/2bndy5/following{/other_user}", + "gists_url": "https://api.github.com/users/2bndy5/gists{/gist_id}", + "starred_url": "https://api.github.com/users/2bndy5/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/2bndy5/subscriptions", + "organizations_url": "https://api.github.com/users/2bndy5/orgs", + "repos_url": "https://api.github.com/users/2bndy5/repos", + "events_url": "https://api.github.com/users/2bndy5/events{/privacy}", + "received_events_url": "https://api.github.com/users/2bndy5/received_events", + "type": "User", + "user_view_type": "public", + "site_admin": false + }, + "parents": [ + { + "sha": "d6831a093b4a6f828e49573e8b048729841cd5c7", + "url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/commits/d6831a093b4a6f828e49573e8b048729841cd5c7", + "html_url": "https://github.com/cpp-linter/test-cpp-linter-action/commit/d6831a093b4a6f828e49573e8b048729841cd5c7" + } + ], + "stats": { + "total": 32, + "additions": 14, + "deletions": 18 + }, + "files": [ + { + "sha": "1bf553e06e4b7c6c9a9be5da4845acbdeb04f6a5", + "filename": "src/demo.cpp", + "status": "modified", + "additions": 11, + "deletions": 10, + "changes": 21, + "blob_url": "https://github.com/cpp-linter/test-cpp-linter-action/blob/01e2ebf010b949986fa3e6d6e81e7f7c8da90001/src%2Fdemo.cpp", + "raw_url": "https://github.com/cpp-linter/test-cpp-linter-action/raw/01e2ebf010b949986fa3e6d6e81e7f7c8da90001/src%2Fdemo.cpp", + "contents_url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/contents/src%2Fdemo.cpp?ref=01e2ebf010b949986fa3e6d6e81e7f7c8da90001", + "patch": "@@ -1,17 +1,18 @@\n /** This is a very ugly test code (doomed to fail linting) */\n #include \"demo.hpp\"\n-#include \n-#include \n+#include \n \n-// using size_t from cstddef\n-size_t dummyFunc(size_t i) { return i; }\n \n-int main()\n-{\n- for (;;)\n- break;\n+\n+\n+int main(){\n+\n+ for (;;) break;\n+\n \n printf(\"Hello world!\\n\");\n \n- return 0;\n-}\n+\n+\n+\n+ return 0;}" + }, + { + "sha": "f93d0122ae2e3c1952c795837d71c432036b55eb", + "filename": "src/demo.hpp", + "status": "modified", + "additions": 3, + "deletions": 8, + "changes": 11, + "blob_url": "https://github.com/cpp-linter/test-cpp-linter-action/blob/01e2ebf010b949986fa3e6d6e81e7f7c8da90001/src%2Fdemo.hpp", + "raw_url": "https://github.com/cpp-linter/test-cpp-linter-action/raw/01e2ebf010b949986fa3e6d6e81e7f7c8da90001/src%2Fdemo.hpp", + "contents_url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/contents/src%2Fdemo.hpp?ref=01e2ebf010b949986fa3e6d6e81e7f7c8da90001", + "patch": "@@ -5,12 +5,10 @@\n class Dummy {\n char* useless;\n int numb;\n+ Dummy() :numb(0), useless(\"\\0\"){}\n \n public:\n- void *not_usefull(char *str){\n- useless = str;\n- return 0;\n- }\n+ void *not_useful(char *str){useless = str;}\n };\n \n \n@@ -28,14 +26,11 @@ class Dummy {\n \n \n \n-\n-\n-\n-\n \n \n struct LongDiff\n {\n+\n long diff;\n \n };" + } + ] +} diff --git a/cpp-linter/tests/comments.rs b/cpp-linter/tests/comments.rs index 628278ef..b77aa13d 100644 --- a/cpp-linter/tests/comments.rs +++ b/cpp-linter/tests/comments.rs @@ -1,6 +1,8 @@ +#![cfg(feature = "bin")] use chrono::Utc; -use cpp_linter::cli::{LinesChangedOnly, ThreadComments}; +use cpp_linter::cli::ThreadComments; use cpp_linter::run::run_main; +use git_bot_feedback::LinesChangedOnly; use mockito::Matcher; use std::{env, fmt::Display, io::Write, path::Path}; use tempfile::NamedTempFile; @@ -8,12 +10,11 @@ use tempfile::NamedTempFile; mod common; use common::{create_test_space, mock_server}; -const SHA: &str = "8d68756375e0483c7ac2b4d6bbbece420dbbb495"; +const SHA: &str = "01e2ebf010b949986fa3e6d6e81e7f7c8da90001"; const REPO: &str = "cpp-linter/test-cpp-linter-action"; -const PR: i64 = 22; +const PR: i64 = 42; const TOKEN: &str = "123456"; const MOCK_ASSETS_PATH: &str = "tests/comment_test_assets/"; -const EVENT_PAYLOAD: &str = "{\"number\": 22}"; const RESET_RATE_LIMIT_HEADER: &str = "x-ratelimit-reset"; const REMAINING_RATE_LIMIT_HEADER: &str = "x-ratelimit-remaining"; @@ -63,19 +64,30 @@ impl Default for TestParams { async fn setup(lib_root: &Path, test_params: &TestParams) { let mut event_payload_path = NamedTempFile::new_in("./").unwrap(); + let tmp_out_file = NamedTempFile::new().unwrap(); unsafe { + env::set_var("GITHUB_ACTIONS", "true"); env::set_var( "GITHUB_EVENT_NAME", test_params.event_t.to_string().as_str(), ); - env::remove_var("GITHUB_OUTPUT"); // avoid writing to GH_OUT in parallel-running tests + env::set_var("GITHUB_OUTPUT", tmp_out_file.path()); env::set_var("GITHUB_REPOSITORY", REPO); env::set_var("GITHUB_SHA", SHA); env::set_var("GITHUB_TOKEN", TOKEN); env::set_var("CI", "true"); if test_params.event_t == EventType::PullRequest { + let event_payload = serde_json::json!({ + "pull_request": { + "draft": false, + "state": "open", + "number": PR, + "locked": false, + } + }) + .to_string(); event_payload_path - .write_all(EVENT_PAYLOAD.as_bytes()) + .write_all(event_payload.as_bytes()) .expect("Failed to create mock event payload."); env::set_var("GITHUB_EVENT_PATH", event_payload_path.path()); } @@ -91,17 +103,18 @@ async fn setup(lib_root: &Path, test_params: &TestParams) { let mut mocks = vec![]; if test_params.lines_changed_only != LinesChangedOnly::Off { - let diff_end_point = if test_params.event_t == EventType::PullRequest { - format!("pulls/{PR}") + let (asset_prefix, diff_end_point) = if test_params.event_t == EventType::PullRequest { + ("pr".to_string(), format!("pulls/{PR}/files")) } else { - format!("commits/{SHA}") + ("push".to_string(), format!("commits/{SHA}")) }; mocks.push( server .mock("GET", format!("/repos/{REPO}/{diff_end_point}").as_str()) - .match_header("Accept", "application/vnd.github.diff") + .match_header("Accept", "application/vnd.github.raw+json") .match_header("Authorization", format!("token {TOKEN}").as_str()) - .with_body_from_file(format!("{asset_path}patch.diff")) + .match_query(Matcher::Any) + .with_body_from_file(format!("{asset_path}{asset_prefix}_diff.json")) .with_header(REMAINING_RATE_LIMIT_HEADER, "50") .with_header(RESET_RATE_LIMIT_HEADER, reset_timestamp.as_str()) .create(), @@ -127,7 +140,7 @@ async fn setup(lib_root: &Path, test_params: &TestParams) { if test_params.bad_existing_comments { mock = mock.with_body(String::new()); } else { - mock = mock.with_body_from_file(format!("{asset_path}push_comments_{SHA}.json")); + mock = mock.with_body_from_file(format!("{asset_path}push_comments.json")); } mock = mock.create(); mocks.push(mock); @@ -232,7 +245,7 @@ async fn setup(lib_root: &Path, test_params: &TestParams) { let mut args = vec![ "cpp-linter".to_string(), "-v=debug".to_string(), - format!("-V={}", env::var("CLANG_VERSION").unwrap_or("".to_string())), + format!("-V={}", env::var("CLANG_VERSION").unwrap_or_default()), format!("-l={}", test_params.lines_changed_only), "--ignore-tidy=src/some source.c".to_string(), "--ignore-format=src/some source.c".to_string(), @@ -244,7 +257,19 @@ async fn setup(lib_root: &Path, test_params: &TestParams) { if test_params.force_lgtm { args.push("-e=c".to_string()); } - run_main(args).await.unwrap(); + let result = run_main(args).await; + match result { + Ok(_) => { + if test_params.bad_existing_comments { + panic!("Expected failure but got success"); + } + } + Err(e) => { + if !test_params.bad_existing_comments { + panic!("Expected success but got failure: {e}"); + } + } + } for mock in mocks { mock.assert(); } diff --git a/cpp-linter/tests/git_status_test_assets/cpp-linter/cpp-linter/950ff0b690e1903797c303c5fc8d9f3b52f1d3c5.diff b/cpp-linter/tests/git_status_test_assets/cpp-linter/cpp-linter/950ff0b690e1903797c303c5fc8d9f3b52f1d3c5.diff deleted file mode 100644 index 6544d4ce..00000000 --- a/cpp-linter/tests/git_status_test_assets/cpp-linter/cpp-linter/950ff0b690e1903797c303c5fc8d9f3b52f1d3c5.diff +++ /dev/null @@ -1,5208 +0,0 @@ -diff --git a/.gitattributes b/.gitattributes -new file mode 100644 -index 0000000..f7c5d6a ---- /dev/null -+++ b/.gitattributes -@@ -0,0 +1,10 @@ -+# Set the default behavior, in case people don't have core.autocrlf set. -+* text=auto -+ -+# Explicitly declare text files you want to always be normalized and converted -+# to native line endings on checkout. -+*.py text eol=lf -+*.rst text eol=lf -+*.sh text eol=lf -+*.cpp text eol=lf -+*.hpp text eol=lf -diff --git a/.github/stale.yml b/.github/stale.yml -new file mode 100644 -index 0000000..0d0b1c9 ---- /dev/null -+++ b/.github/stale.yml -@@ -0,0 +1 @@ -+_extends: .github -diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml -new file mode 100644 -index 0000000..33f08a3 ---- /dev/null -+++ b/.github/workflows/build-docs.yml -@@ -0,0 +1,33 @@ -+name: Docs -+ -+on: [push, workflow_dispatch] -+ -+jobs: -+ build: -+ runs-on: ubuntu-latest -+ steps: -+ - uses: actions/checkout@v3 -+ -+ - uses: actions/setup-python@v4 -+ with: -+ python-version: 3.x -+ -+ - name: Install docs dependencies -+ run: pip install . -r docs/requirements.txt -+ -+ - name: Build docs -+ run: sphinx-build docs docs/_build/html -+ -+ - name: Deploy to gh-pages -+ uses: actions/upload-artifact@v3 -+ with: -+ name: "cpp-linter_docs" -+ path: ${{ github.workspace }}/docs/_build/html -+ -+ - name: upload to github pages -+ # only publish doc changes from main branch -+ if: github.ref == 'refs/heads/main' -+ uses: peaceiris/actions-gh-pages@v3 -+ with: -+ github_token: ${{ secrets.GITHUB_TOKEN }} -+ publish_dir: ./docs/_build/html -diff --git a/.github/workflows/pre-commit-hooks.yml b/.github/workflows/pre-commit-hooks.yml -new file mode 100644 -index 0000000..e9426fc ---- /dev/null -+++ b/.github/workflows/pre-commit-hooks.yml -@@ -0,0 +1,17 @@ -+name: Pre-commit -+ -+on: -+ push: -+ pull_request: -+ types: opened -+ -+jobs: -+ check-source-files: -+ runs-on: ubuntu-latest -+ steps: -+ - uses: actions/checkout@v3 -+ - uses: actions/setup-python@v4 -+ with: -+ python-version: '3.x' -+ - run: python3 -m pip install pre-commit -+ - run: pre-commit run --all-files -diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml -new file mode 100644 -index 0000000..278717b ---- /dev/null -+++ b/.github/workflows/publish-pypi.yml -@@ -0,0 +1,51 @@ -+# This workflow will upload a Python Package using Twine when a release is created -+# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries -+ -+# This workflow uses actions that are not certified by GitHub. -+# They are provided by a third-party and are governed by -+# separate terms of service, privacy policy, and support -+# documentation. -+ -+name: Upload Python Package -+ -+on: -+ release: -+ branches: [master] -+ types: [published] -+ workflow_dispatch: -+ -+permissions: -+ contents: read -+ -+jobs: -+ deploy: -+ -+ runs-on: ubuntu-latest -+ -+ steps: -+ - uses: actions/checkout@v3 -+ # use fetch --all for setuptools_scm to work -+ with: -+ fetch-depth: 0 -+ - name: Set up Python -+ uses: actions/setup-python@v4 -+ with: -+ python-version: '3.x' -+ - name: Install dependencies -+ run: python -m pip install --upgrade pip twine -+ - name: Build wheel -+ run: python -m pip wheel -w dist --no-deps . -+ - name: Check distribution -+ run: twine check dist/* -+ - name: Publish package (to TestPyPI) -+ if: github.event_name == 'workflow_dispatch' && github.repository == 'cpp-linter/cpp-linter' -+ env: -+ TWINE_USERNAME: __token__ -+ TWINE_PASSWORD: ${{ secrets.TEST_PYPI_TOKEN }} -+ run: twine upload --repository testpypi dist/* -+ - name: Publish package (to PyPI) -+ if: github.event_name != 'workflow_dispatch' && github.repository == 'cpp-linter/cpp-linter' -+ env: -+ TWINE_USERNAME: __token__ -+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} -+ run: twine upload dist/* -diff --git a/.github/workflows/run-dev-tests.yml b/.github/workflows/run-dev-tests.yml -new file mode 100644 -index 0000000..6c2dd88 ---- /dev/null -+++ b/.github/workflows/run-dev-tests.yml -@@ -0,0 +1,111 @@ -+name: "Check python code" -+ -+on: -+ push: -+ paths: -+ - "**.py" -+ - pyproject.toml -+ - ".github/workflows/run-dev-tests.yml" -+ pull_request: -+ types: opened -+ paths: -+ - "**.py" -+ - "**requirements*.txt" -+ - pyproject.toml -+ - ".github/workflows/run-dev-tests.yml" -+ -+jobs: -+ build: -+ runs-on: ubuntu-latest -+ steps: -+ - uses: actions/checkout@v3 -+ - uses: actions/setup-python@v4 -+ with: -+ python-version: '3.x' -+ - name: Build wheel -+ run: python3 -m pip wheel --no-deps -w dist . -+ - name: Upload wheel as artifact -+ uses: actions/upload-artifact@v3 -+ with: -+ name: cpp-linter_wheel -+ path: ${{ github.workspace }}/dist/*.whl -+ -+ test: -+ needs: [build] -+ strategy: -+ fail-fast: false -+ matrix: -+ py: ['3.7', '3.8', '3.9', '3.10'] -+ os: ['windows-latest', ubuntu-latest] -+ version: ['13', '12', '11', '10', '9', '8', '7'] -+ include: -+ - tools_dir: 'N/A' -+ - os: 'windows-latest' -+ version: '10' -+ tools_dir: temp -+ - os: 'windows-latest' -+ version: '10' -+ tools_dir: temp -+ - os: 'windows-latest' -+ version: '11' -+ tools_dir: temp -+ - os: 'windows-latest' -+ version: '12' -+ tools_dir: temp -+ - os: 'ubuntu-latest' -+ version: '13' -+ tools_dir: temp -+ # - version: '14' -+ # tools_dir: temp -+ - version: '7' -+ tools_dir: temp -+ - version: '8' -+ tools_dir: temp -+ - version: '9' -+ tools_dir: temp -+ -+ runs-on: ${{ matrix.os }} -+ steps: -+ - uses: actions/checkout@v3 -+ -+ - uses: actions/setup-python@v4 -+ with: -+ python-version: ${{ matrix.py }} -+ -+ - name: download wheel artifact -+ uses: actions/download-artifact@v3 -+ with: -+ name: cpp-linter_wheel -+ path: dist -+ -+ - name: Install workflow deps -+ # using a wildcard as filename on Windows requires a bash shell -+ shell: bash -+ run: python3 -m pip install pytest coverage[toml] dist/*.whl -+ -+ - name: Install clang-tools -+ if: matrix.tools_dir == 'temp' -+ run: | -+ python -m pip install clang-tools -+ clang-tools --install ${{ matrix.version }} --directory ${{ runner.temp }}/clang-tools -+ -+ - name: Collect Coverage (native clang install) -+ if: matrix.tools_dir == 'N/A' -+ env: -+ CLANG_VERSION: ${{ matrix.version }} -+ run: coverage run -m pytest -+ -+ - name: Collect Coverage (non-native clang install) -+ if: matrix.tools_dir == 'temp' -+ env: -+ CLANG_VERSION: ${{ runner.temp }}/clang-tools -+ run: coverage run -m pytest -+ -+ - run: coverage report && coverage xml -+ -+ - uses: codecov/codecov-action@v3 -+ if: matrix.os == 'ubuntu-latest' && matrix.version == '12' && matrix.py == '3.10' -+ with: -+ files: ./coverage.xml -+ fail_ci_if_error: true # optional (default = false) -+ verbose: true # optional (default = false) -diff --git a/.gitignore b/.gitignore -new file mode 100644 -index 0000000..3065187 ---- /dev/null -+++ b/.gitignore -@@ -0,0 +1,188 @@ -+# local demo specific -+.changed_files.json -+clang_format*.txt -+clang_tidy*.txt -+act.exe -+clang_tidy_output.yml -+clang_format_output.xml -+event_payload.json -+comments.json -+ -+#### ignores for Python -+# Byte-compiled / optimized / DLL files -+__pycache__/ -+*.py[cod] -+*$py.class -+ -+# Distribution / packaging -+.Python -+build/ -+develop-eggs/ -+dist/ -+downloads/ -+eggs/ -+.eggs/ -+lib/ -+lib64/ -+parts/ -+sdist/ -+var/ -+wheels/ -+pip-wheel-metadata/ -+share/python-wheels/ -+*.egg-info/ -+.installed.cfg -+*.egg -+MANIFEST -+ -+# PyInstaller -+# Usually these files are written by a python script from a template -+# before PyInstaller builds the exe, so as to inject date/other infos into it. -+*.manifest -+*.spec -+ -+# Installer logs -+pip-log.txt -+pip-delete-this-directory.txt -+ -+# Unit test / coverage reports -+htmlcov/ -+.tox/ -+.nox/ -+.coverage -+.coverage.* -+.cache -+nosetests.xml -+coverage.xml -+*.cover -+*.py,cover -+.hypothesis/ -+.pytest_cache/ -+tests/*/test*.json -+tests/**/*.c -+ -+# Translations -+*.mo -+*.pot -+ -+# Django stuff: -+*.log -+local_settings.py -+db.sqlite3 -+db.sqlite3-journal -+ -+# Flask stuff: -+instance/ -+.webassets-cache -+ -+# Scrapy stuff: -+.scrapy -+ -+# Sphinx documentation -+docs/_build/ -+ -+# PyBuilder -+target/ -+ -+# Jupyter Notebook -+.ipynb_checkpoints -+ -+# IPython -+profile_default/ -+ipython_config.py -+ -+# pyenv -+.python-version -+ -+# pipenv -+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -+# However, in case of collaboration, if having platform-specific dependencies or dependencies -+# having no cross-platform support, pipenv may install dependencies that don't work, or not -+# install all needed dependencies. -+#Pipfile.lock -+ -+# PEP 582; used by e.g. github.com/David-OConnor/pyflow -+__pypackages__/ -+ -+# Celery stuff -+celerybeat-schedule -+celerybeat.pid -+ -+# SageMath parsed files -+*.sage.py -+ -+# Environments -+.env -+.venv -+env/ -+venv/ -+ENV/ -+env.bak/ -+venv.bak/ -+ -+# Spyder project settings -+.spyderproject -+.spyproject -+ -+# Rope project settings -+.ropeproject -+ -+# mkdocs documentation -+/site -+ -+# mypy -+.mypy_cache/ -+.dmypy.json -+dmypy.json -+ -+# Pyre type checker -+.pyre/ -+ -+# exclude local VSCode's settings folder -+.vscode/ -+ -+#### ignores for C++ -+# Prerequisites -+*.d -+ -+# Compiled Object files -+*.slo -+*.lo -+*.o -+*.obj -+ -+# Precompiled Headers -+*.gch -+*.pch -+ -+# Compiled Dynamic libraries -+*.so -+*.dylib -+*.dll -+ -+# Fortran module files -+*.mod -+*.smod -+ -+# Compiled Static libraries -+*.lai -+*.la -+*.a -+*.lib -+ -+# Executables -+*.exe -+*.out -+*.app -+ -+# Cmake build-in-source generated stuff -+CMakeUserPresets.json -+CMakeCache.txt -+CPackConfig.cmake -+CPackSourceConfig.cmake -+CMakeFiles -+cmake_install.cmake -+ -+ -+# generated doc pages -+docs/cli_args.rst -diff --git a/.gitpod.yml b/.gitpod.yml -new file mode 100644 -index 0000000..c21ea2f ---- /dev/null -+++ b/.gitpod.yml -@@ -0,0 +1,2 @@ -+tasks: -+ - init: pip install -r requirements.txt -diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml -new file mode 100644 -index 0000000..4ccf7ab ---- /dev/null -+++ b/.pre-commit-config.yaml -@@ -0,0 +1,38 @@ -+repos: -+ - repo: https://github.com/pre-commit/pre-commit-hooks -+ rev: v4.3.0 -+ hooks: -+ - id: trailing-whitespace -+ - id: end-of-file-fixer -+ - id: check-docstring-first -+ - id: check-added-large-files -+ - id: check-yaml -+ - id: check-toml -+ - id: requirements-txt-fixer -+ - id: mixed-line-ending -+ args: ["--fix=lf"] -+ - repo: https://github.com/python/black -+ rev: '22.6.0' -+ hooks: -+ - id: black -+ args: ["--diff"] -+ - repo: https://github.com/pycqa/pylint -+ rev: v2.14.5 -+ hooks: -+ - id: pylint -+ name: pylint (action code) -+ types: [python] -+ exclude: "^(docs/|tests/|setup.py$)" -+ additional_dependencies: [pyyaml, requests] -+ - repo: local -+ # this is a "local" hook to run mypy (see https://pre-commit.com/#repository-local-hooks) -+ # because the mypy project doesn't seem to be compatible with pre-commit hooks -+ hooks: -+ - id: mypy -+ name: mypy -+ description: type checking with mypy tool -+ language: python -+ types: [python] -+ entry: mypy -+ exclude: "^(docs/|setup.py$)" -+ additional_dependencies: [mypy, types-pyyaml, types-requests, rich, requests, pytest, pyyaml, '.'] -diff --git a/README.rst b/README.rst -new file mode 100644 -index 0000000..c0b5ae3 ---- /dev/null -+++ b/README.rst -@@ -0,0 +1,34 @@ -+C/C++ Linting Package -+===================== -+ -+.. image:: https://img.shields.io/github/v/release/cpp-linter/cpp-linter?style=plastic -+ :alt: Latest Version -+ :target: https://github.com/cpp-linter/cpp-linter/releases -+.. image:: https://img.shields.io/github/license/cpp-linter/cpp-linter?label=license&logo=github&style=plastic -+ :alt: License -+ :target: https://github.com/cpp-linter/cpp-linter/blob/main/LICENSE -+.. image:: https://codecov.io/gh/cpp-linter/cpp-linter/branch/main/graph/badge.svg?token=0814O9WHQU -+ :alt: CodeCov -+ :target: https://codecov.io/gh/cpp-linter/cpp-linter -+.. image:: https://github.com/cpp-linter/cpp-linter/actions/workflows/build-docs.yml/badge.svg -+ :alt: Docs -+ :target: https://cpp-linter.github.io/cpp-linter -+ -+A Python package for linting C/C++ code with clang-tidy and/or clang-format to collect feedback provided in the form of thread comments and/or file annotations. -+ -+Usage -+----- -+ -+For usage in a CI workflow, see `the cpp-linter/cpp-linter-action repository `_ -+ -+For the description of supported Command Line Interface options, see `the CLI documentation `_ -+ -+Have question or feedback? -+-------------------------- -+ -+To provide feedback (requesting a feature or reporting a bug) please post to `issues `_. -+ -+License -+------- -+ -+The scripts and documentation in this project are released under the `MIT License `_. -diff --git a/cpp_linter/__init__.py b/cpp_linter/__init__.py -new file mode 100644 -index 0000000..5921623 ---- /dev/null -+++ b/cpp_linter/__init__.py -@@ -0,0 +1,157 @@ -+"""The Base module of the :mod:`cpp_linter` package. This holds the objects shared by -+multiple modules.""" -+import os -+from pathlib import Path -+import platform -+import logging -+from typing import TYPE_CHECKING, List, Dict, Tuple, Any -+from requests import Response -+ -+if TYPE_CHECKING: # Used to avoid circular imports -+ from cpp_linter.clang_format_xml import XMLFixit -+ from cpp_linter.clang_tidy_yml import YMLFixit -+ from cpp_linter.clang_tidy import TidyNotification -+ -+FOUND_RICH_LIB = False -+try: -+ from rich.logging import RichHandler -+ -+ FOUND_RICH_LIB = True -+ -+ logging.basicConfig( -+ format="%(name)s: %(message)s", -+ handlers=[RichHandler(show_time=False)], -+ ) -+ -+except ImportError: # pragma: no cover -+ logging.basicConfig() -+ -+#: The logging.Logger object used for outputting data. -+logger = logging.getLogger("CPP Linter") -+if not FOUND_RICH_LIB: -+ logger.debug("rich module not found") -+ -+# global constant variables -+IS_ON_RUNNER = bool(os.getenv("CI")) -+GITHUB_SHA = os.getenv("GITHUB_SHA", "") -+GITHUB_TOKEN = os.getenv("GITHUB_TOKEN", os.getenv("GIT_REST_API", "")) -+API_HEADERS = { -+ "Accept": "application/vnd.github.v3.text+json", -+} -+if GITHUB_TOKEN: -+ API_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}" -+IS_ON_WINDOWS = platform.system().lower() == "windows" -+ -+ -+class Globals: -+ """Global variables for re-use (non-constant).""" -+ -+ PAYLOAD_TIDY: str = "" -+ """The accumulated output of clang-tidy (gets appended to OUTPUT)""" -+ OUTPUT: str = "" -+ """The accumulated body of the resulting comment that gets posted.""" -+ FILES: List[Dict[str, Any]] = [] -+ """The responding payload containing info about changed files.""" -+ EVENT_PAYLOAD: Dict[str, Any] = {} -+ """The parsed JSON of the event payload.""" -+ response_buffer: Response = Response() -+ """A shared response object for `requests` module.""" -+ -+ -+class GlobalParser: -+ """Global variables specific to output parsers. Each element in each of the -+ following attributes represents a clang-tool's output for 1 source file. -+ """ -+ -+ tidy_notes = [] # type: List[TidyNotification] -+ """This can only be a `list` of type -+ :class:`~cpp_linter.clang_tidy.TidyNotification`.""" -+ tidy_advice = [] # type: List[YMLFixit] -+ """This can only be a `list` of type :class:`~cpp_linter.clang_tidy_yml.YMLFixit`. -+ """ -+ format_advice = [] # type: List[XMLFixit] -+ """This can only be a `list` of type :class:`~cpp_linter.clang_format_xml.XMLFixit`. -+ """ -+ -+ -+def get_line_cnt_from_cols(file_path: str, offset: int) -> Tuple[int, int]: -+ """Gets a line count and columns offset from a file's absolute offset. -+ -+ :param file_path: Path to file. -+ :param offset: The byte offset to translate -+ -+ :returns: -+ A `tuple` of 2 `int` numbers: -+ -+ - Index 0 is the line number for the given offset. -+ - Index 1 is the column number for the given offset on the line. -+ """ -+ # logger.debug("Getting line count from %s at offset %d", file_path, offset) -+ contents = Path(file_path).read_bytes()[:offset] -+ return (contents.count(b"\n") + 1, offset - contents.rfind(b"\n")) -+ -+ -+def range_of_changed_lines( -+ file_obj: Dict[str, Any], lines_changed_only: int -+) -> List[int]: -+ """Assemble a list of lines changed. -+ -+ :param file_obj: The file's JSON object. -+ :param lines_changed_only: A flag to indicate the focus of certain lines. -+ -+ - ``0``: focuses on all lines in file. -+ - ``1``: focuses on any lines shown in the event's diff (may include -+ unchanged lines). -+ - ``2``: focuses strictly on lines in the diff that contain additions. -+ :returns: -+ A list of line numbers for which to give attention. -+ """ -+ if lines_changed_only: -+ ranges = file_obj["line_filter"][ -+ "diff_chunks" if lines_changed_only == 1 else "lines_added" -+ ] -+ return [l for r in ranges for l in range(r[0], r[1])] -+ return [] -+ -+ -+def log_response_msg() -> bool: -+ """Output the response buffer's message on a failed request. -+ -+ :returns: -+ A bool describing if response's status code was less than 400. -+ """ -+ if Globals.response_buffer.status_code >= 400: -+ logger.error( -+ "response returned %d message: %s", -+ Globals.response_buffer.status_code, -+ Globals.response_buffer.text, -+ ) -+ return False -+ return True -+ -+ -+def assemble_version_exec(tool_name: str, specified_version: str) -> str: -+ """Assembles the command to the executable of the given clang tool based on given -+ version information. -+ -+ :param tool_name: The name of the clang tool to be executed. -+ :param specified_version: The version number or the installed path to a version of -+ the tool's executable. -+ """ -+ suffix = ".exe" if IS_ON_WINDOWS else "" -+ if specified_version.isdigit(): # version info is not a path -+ # let's assume the exe is in the PATH env var -+ if IS_ON_WINDOWS: -+ # installs don't usually append version number to exe name on Windows -+ return f"{tool_name}{suffix}" # omit version number -+ return f"{tool_name}-{specified_version}{suffix}" -+ version_path = Path(specified_version).resolve() # make absolute -+ for path in [ -+ # if installed via KyleMayes/install-llvm-action using the `directory` option -+ version_path / "bin" / (tool_name + suffix), -+ # if installed via clang-tools-pip pkg using the `-d` option -+ version_path / (tool_name + suffix), -+ ]: -+ if path.exists(): -+ return str(path) -+ return tool_name + suffix -diff --git a/cpp_linter/clang_format_xml.py b/cpp_linter/clang_format_xml.py -new file mode 100644 -index 0000000..0554264 ---- /dev/null -+++ b/cpp_linter/clang_format_xml.py -@@ -0,0 +1,137 @@ -+"""Parse output from clang-format's XML suggestions.""" -+from pathlib import PurePath -+from typing import List, Optional -+import xml.etree.ElementTree as ET -+from . import GlobalParser, get_line_cnt_from_cols -+ -+ -+class FormatReplacement: -+ """An object representing a single replacement. -+ -+ :param cols: The columns number of where the suggestion starts on the line -+ :param null_len: The number of bytes removed by suggestion -+ :param text: The `bytearray` of the suggestion -+ """ -+ -+ def __init__(self, cols: int, null_len: int, text: str) -> None: -+ #: The columns number of where the suggestion starts on the line -+ self.cols = cols -+ #: The number of bytes removed by suggestion -+ self.null_len = null_len -+ #: The `bytearray` of the suggestion -+ self.text = text -+ -+ def __repr__(self) -> str: -+ return ( -+ f"" -+ ) -+ -+ -+class FormatReplacementLine: -+ """An object that represents a replacement(s) for a single line. -+ -+ :param line_numb: The line number of about the replacements -+ """ -+ -+ def __init__(self, line_numb: int): -+ #: The line number of where the suggestion starts -+ self.line = line_numb -+ -+ #: A list of `FormatReplacement` object(s) representing suggestions. -+ self.replacements: List[FormatReplacement] = [] -+ -+ def __repr__(self): -+ return ( -+ f"" -+ ) -+ -+ -+class XMLFixit: -+ """A single object to represent each suggestion. -+ -+ :param filename: The source file's name for which the contents of the xml -+ file exported by clang-tidy. -+ """ -+ -+ def __init__(self, filename: str): -+ """ """ -+ #: The source file that the suggestion concerns. -+ self.filename = PurePath(filename).as_posix() -+ -+ self.replaced_lines: List[FormatReplacementLine] = [] -+ """A list of `FormatReplacementLine` representing replacement(s) -+ on a single line.""" -+ -+ def __repr__(self) -> str: -+ return ( -+ f"" -+ ) -+ -+ def log_command(self, style: str, line_filter: List[int]) -> Optional[str]: -+ """Output a notification as a github log command. -+ -+ .. seealso:: -+ -+ - `An error message `_ -+ - `A warning message `_ -+ - `A notice message `_ -+ -+ :param style: The chosen code style guidelines. -+ :param line_filter: A list of lines numbers used to narrow notifications. -+ """ -+ if style not in ("llvm", "google", "webkit", "mozilla", "gnu"): -+ # potentially the style parameter could be a str of JSON/YML syntax -+ style = "Custom" -+ else: -+ if style.startswith("llvm") or style.startswith("gnu"): -+ style = style.upper() -+ else: -+ style = style.title() -+ line_list = [] -+ for fix in self.replaced_lines: -+ if not line_filter or (line_filter and fix.line in line_filter): -+ line_list.append(str(fix.line)) -+ if not line_list: -+ return None -+ return ( -+ "::notice file={name},title=Run clang-format on {name}::" -+ "File {name} (lines {lines}): Code does not conform to {style_guide} " -+ "style guidelines.".format( -+ name=self.filename, -+ lines=", ".join(line_list), -+ style_guide=style, -+ ) -+ ) -+ -+ -+def parse_format_replacements_xml(src_filename: str): -+ """Parse XML output of replacements from clang-format. Output is saved to -+ :attr:`~cpp_linter.GlobalParser.format_advice`. -+ -+ :param src_filename: The source file's name for which the contents of the xml -+ file exported by clang-tidy. -+ """ -+ tree = ET.parse("clang_format_output.xml") -+ fixit = XMLFixit(src_filename) -+ for child in tree.getroot(): -+ if child.tag == "replacement": -+ offset = int(child.attrib["offset"]) -+ line, cols = get_line_cnt_from_cols(src_filename, offset) -+ null_len = int(child.attrib["length"]) -+ text = "" if child.text is None else child.text -+ fix = FormatReplacement(cols, null_len, text) -+ if not fixit.replaced_lines or ( -+ fixit.replaced_lines and line != fixit.replaced_lines[-1].line -+ ): -+ line_fix = FormatReplacementLine(line) -+ line_fix.replacements.append(fix) -+ fixit.replaced_lines.append(line_fix) -+ elif fixit.replaced_lines and line == fixit.replaced_lines[-1].line: -+ fixit.replaced_lines[-1].replacements.append(fix) -+ GlobalParser.format_advice.append(fixit) -diff --git a/cpp_linter/clang_tidy.py b/cpp_linter/clang_tidy.py -new file mode 100644 -index 0000000..11c1ae8 ---- /dev/null -+++ b/cpp_linter/clang_tidy.py -@@ -0,0 +1,116 @@ -+"""Parse output from clang-tidy's stdout""" -+from pathlib import Path, PurePath -+import re -+from typing import Tuple, Union, List, cast -+from . import GlobalParser -+ -+NOTE_HEADER = re.compile(r"^(.*):(\d+):(\d+):\s(\w+):(.*)\[(.*)\]$") -+ -+ -+class TidyNotification: -+ """Create a object that decodes info from the clang-tidy output's initial line that -+ details a specific notification. -+ -+ :param notification_line: The first line in the notification parsed into a -+ `tuple` of `str` that represent the different components of the -+ notification's details. -+ """ -+ -+ def __init__( -+ self, -+ notification_line: Tuple[str, Union[int, str], Union[int, str], str, str, str], -+ ): -+ # logger.debug("Creating tidy note from line %s", notification_line) -+ ( -+ self.filename, -+ self.line, -+ #: The columns of the line that triggered the notification. -+ self.cols, -+ self.note_type, -+ self.note_info, -+ #: The clang-tidy check that enabled the notification. -+ self.diagnostic, -+ ) = notification_line -+ -+ #: The rationale of the notification. -+ self.note_info = self.note_info.strip() -+ #: The priority level of notification (warning/error). -+ self.note_type = self.note_type.strip() -+ #: The line number of the source file. -+ self.line = int(self.line) -+ self.cols = int(self.cols) -+ #: The source filename concerning the notification. -+ self.filename = ( -+ PurePath(self.filename).as_posix().replace(Path.cwd().as_posix(), "") -+ ) -+ #: A `list` of lines for the code-block in the notification. -+ self.fixit_lines: List[str] = [] -+ -+ def __repr__(self) -> str: -+ concerned_code = "" -+ if self.fixit_lines: -+ if not self.fixit_lines[-1].endswith("\n"): -+ # some notifications' code-blocks don't end in a LF -+ self.fixit_lines[-1] += "\n" # and they should for us -+ concerned_code = "```{}\n{}```\n".format( -+ PurePath(self.filename).suffix.lstrip("."), -+ "\n".join(self.fixit_lines), -+ ) -+ return ( -+ "
\n{}:{}:{}: {}: [{}]" -+ "\n\n> {}\n

\n\n{}

\n
\n\n".format( -+ self.filename, -+ self.line, -+ self.cols, -+ self.note_type, -+ self.diagnostic, -+ self.note_info, -+ concerned_code, -+ ) -+ ) -+ -+ def log_command(self) -> str: -+ """Output the notification as a github log command. -+ -+ .. seealso:: -+ -+ - `An error message `_ -+ - `A warning message `_ -+ - `A notice message `_ -+ """ -+ filename = self.filename.replace("\\", "/") -+ return ( -+ "::{} file={file},line={line},title={file}:{line}:{cols} [{diag}]::" -+ "{info}".format( -+ "notice" if self.note_type.startswith("note") else self.note_type, -+ file=filename, -+ line=self.line, -+ cols=self.cols, -+ diag=self.diagnostic, -+ info=self.note_info, -+ ) -+ ) -+ -+ -+def parse_tidy_output() -> None: -+ """Parse clang-tidy output in a file created from stdout. The results are -+ saved to :attr:`~cpp_linter.GlobalParser.tidy_notes`.""" -+ notification = None -+ tidy_out = Path("clang_tidy_report.txt").read_text(encoding="utf-8") -+ for line in tidy_out.splitlines(): -+ match = re.match(NOTE_HEADER, line) -+ if match is not None: -+ notification = TidyNotification( -+ cast( -+ Tuple[str, Union[int, str], Union[int, str], str, str, str], -+ match.groups(), -+ ) -+ ) -+ GlobalParser.tidy_notes.append(notification) -+ elif notification is not None: -+ # append lines of code that are part of -+ # the previous line's notification -+ notification.fixit_lines.append(line) -diff --git a/cpp_linter/clang_tidy_yml.py b/cpp_linter/clang_tidy_yml.py -new file mode 100644 -index 0000000..21b38a0 ---- /dev/null -+++ b/cpp_linter/clang_tidy_yml.py -@@ -0,0 +1,120 @@ -+"""Parse output from clang-tidy's YML format""" -+from pathlib import Path, PurePath -+from typing import List, cast, Dict, Any -+import yaml -+from . import GlobalParser, get_line_cnt_from_cols, logger -+ -+ -+CWD_HEADER_GUARD = bytes( -+ "_".join([p.upper().replace("-", "_") for p in Path.cwd().parts]), encoding="utf-8" -+) #: The constant used to trim absolute paths from header guard suggestions. -+ -+ -+class TidyDiagnostic: -+ """Create an object that represents a diagnostic output found in the -+ YAML exported from clang-tidy. -+ -+ :param diagnostic_name: The name of the check that got triggered. -+ """ -+ -+ def __init__(self, diagnostic_name: str): -+ #: The diagnostic name -+ self.name = diagnostic_name -+ #: The diagnostic message -+ self.message = "" -+ #: The line number that triggered the diagnostic -+ self.line = 0 -+ #: The columns of the `line` that triggered the diagnostic -+ self.cols = 0 -+ #: The number of bytes replaced by suggestions -+ self.null_len = 0 -+ #: The `list` of `TidyReplacement` objects. -+ self.replacements: List["TidyReplacement"] = [] -+ -+ def __repr__(self): -+ """A str representation of all attributes.""" -+ return ( -+ f"" -+ ) -+ -+ -+class TidyReplacement: -+ """Create an object representing a clang-tidy suggested replacement. -+ -+ :param line_cnt: The replacement content's starting line -+ :param cols: The replacement content's starting columns -+ :param length: The number of bytes discarded from `cols` -+ """ -+ -+ def __init__(self, line_cnt: int, cols: int, length: int): -+ #: The replacement content's starting line -+ self.line = line_cnt -+ #: The replacement content's starting columns -+ self.cols = cols -+ #: The number of bytes discarded from `cols` -+ self.null_len = length -+ #: The replacement content's text. -+ self.text: bytes = b"" -+ -+ def __repr__(self) -> str: -+ return ( -+ f"" -+ ) -+ -+ -+class YMLFixit: -+ """A single object to represent each suggestion. -+ -+ :param filename: The source file's name (with path) concerning the suggestion. -+ """ -+ -+ def __init__(self, filename: str) -> None: -+ #: The source file's name concerning the suggestion. -+ self.filename = PurePath(filename).relative_to(Path.cwd()).as_posix() -+ #: The `list` of `TidyDiagnostic` objects. -+ self.diagnostics: List[TidyDiagnostic] = [] -+ -+ def __repr__(self) -> str: -+ return ( -+ f"" -+ ) -+ -+ -+def parse_tidy_suggestions_yml(): -+ """Read a YAML file from clang-tidy and create a list of suggestions from it. -+ Output is saved to :attr:`~cpp_linter.GlobalParser.tidy_advice`. -+ """ -+ yml_file = Path("clang_tidy_output.yml").read_text(encoding="utf-8") -+ yml = yaml.safe_load(yml_file) -+ fixit = YMLFixit(yml["MainSourceFile"]) -+ -+ for diag_results in yml["Diagnostics"]: -+ diag = TidyDiagnostic(diag_results["DiagnosticName"]) -+ if "DiagnosticMessage" in cast(Dict[str, Any], diag_results).keys(): -+ msg = diag_results["DiagnosticMessage"]["Message"] -+ offset = diag_results["DiagnosticMessage"]["FileOffset"] -+ replacements = diag_results["DiagnosticMessage"]["Replacements"] -+ else: # prior to clang-tidy v9, the YML output was structured differently -+ msg = diag_results["Message"] -+ offset = diag_results["FileOffset"] -+ replacements = diag_results["Replacements"] -+ diag.message = msg -+ diag.line, diag.cols = get_line_cnt_from_cols(yml["MainSourceFile"], offset) -+ for replacement in [] if replacements is None else replacements: -+ line_cnt, cols = get_line_cnt_from_cols( -+ yml["MainSourceFile"], replacement["Offset"] -+ ) -+ fix = TidyReplacement(line_cnt, cols, replacement["Length"]) -+ fix.text = bytes(replacement["ReplacementText"], encoding="utf-8") -+ if fix.text.startswith(b"header is missing header guard"): -+ logger.debug( -+ "filtering header guard suggestion (making relative to repo root)" -+ ) -+ fix.text = fix.text.replace(CWD_HEADER_GUARD, b"") -+ diag.replacements.append(fix) -+ fixit.diagnostics.append(diag) -+ # filter out absolute header guards -+ GlobalParser.tidy_advice.append(fixit) -diff --git a/cpp_linter/run.py b/cpp_linter/run.py -new file mode 100644 -index 0000000..d98281e ---- /dev/null -+++ b/cpp_linter/run.py -@@ -0,0 +1,984 @@ -+"""Run clang-tidy and clang-format on a list of changed files provided by GitHub's -+REST API. If executed from command-line, then `main()` is the entrypoint. -+ -+.. seealso:: -+ -+ - `github rest API reference for pulls -+ `_ -+ - `github rest API reference for repos -+ `_ -+ - `github rest API reference for issues -+ `_ -+""" -+import subprocess -+from pathlib import Path, PurePath -+import os -+import sys -+import argparse -+import configparser -+import json -+from typing import cast, List, Dict, Any, Tuple -+import requests -+from . import ( -+ Globals, -+ GlobalParser, -+ logging, -+ logger, -+ GITHUB_TOKEN, -+ GITHUB_SHA, -+ API_HEADERS, -+ IS_ON_RUNNER, -+ log_response_msg, -+ range_of_changed_lines, -+ assemble_version_exec, -+) -+from .clang_tidy_yml import parse_tidy_suggestions_yml -+from .clang_format_xml import parse_format_replacements_xml -+from .clang_tidy import parse_tidy_output, TidyNotification -+from .thread_comments import remove_bot_comments, list_diff_comments # , get_review_id -+ -+ -+# global constant variables -+GITHUB_EVENT_PATH = os.getenv("GITHUB_EVENT_PATH", "") -+GITHUB_API_URL = os.getenv("GITHUB_API_URL", "https://api.github.com") -+GITHUB_REPOSITORY = os.getenv("GITHUB_REPOSITORY", "") -+GITHUB_EVENT_NAME = os.getenv("GITHUB_EVENT_NAME", "unknown") -+GITHUB_WORKSPACE = os.getenv("GITHUB_WORKSPACE", "") -+IS_USING_DOCKER = os.getenv("USING_CLANG_TOOLS_DOCKER", os.getenv("CLANG_VERSIONS")) -+RUNNER_WORKSPACE = "/github/workspace" if IS_USING_DOCKER else GITHUB_WORKSPACE -+ -+# setup CLI args -+cli_arg_parser = argparse.ArgumentParser( -+ description=__doc__[: __doc__.find("If executed from")], -+ formatter_class=argparse.RawTextHelpFormatter, -+) -+arg = cli_arg_parser.add_argument( -+ "-v", -+ "--verbosity", -+ type=int, -+ default=10, -+ help="""This controls the action's verbosity in the workflow's logs. -+Supported options are defined by the `logging-level `_. -+This option does not affect the verbosity of resulting -+thread comments or file annotations. -+ -+Defaults to level ``%(default)s`` (aka """, -+) -+assert arg.help is not None -+arg.help += f"``logging.{logging.getLevelName(arg.default)}``)." -+cli_arg_parser.add_argument( -+ "-p", -+ "--database", -+ default="", -+ help="""The path that is used to read a compile command database. -+For example, it can be a CMake build directory in which a file named -+compile_commands.json exists (set ``CMAKE_EXPORT_COMPILE_COMMANDS`` to ``ON``). -+When no build path is specified, a search for compile_commands.json will be -+attempted through all parent paths of the first input file. See -+https://clang.llvm.org/docs/HowToSetupToolingForLLVM.html for an -+example of setting up Clang Tooling on a source tree.""", -+) -+cli_arg_parser.add_argument( -+ "-s", -+ "--style", -+ default="llvm", -+ help="""The style rules to use (defaults to ``%(default)s``). -+ -+- Set this to ``file`` to have clang-format use the closest relative -+ .clang-format file. -+- Set this to a blank string (``""``) to disable using clang-format -+ entirely.""", -+) -+cli_arg_parser.add_argument( -+ "-c", -+ "--tidy-checks", -+ default="boost-*,bugprone-*,performance-*,readability-*,portability-*,modernize-*," -+ "clang-analyzer-*,cppcoreguidelines-*", -+ help="""A comma-separated list of globs with optional ``-`` prefix. -+Globs are processed in order of appearance in the list. -+Globs without ``-`` prefix add checks with matching names to the set, -+globs with the ``-`` prefix remove checks with matching names from the set of -+enabled checks. This option's value is appended to the value of the 'Checks' -+option in a .clang-tidy file (if any). -+ -+- It is possible to disable clang-tidy entirely by setting this option to ``'-*'``. -+- It is also possible to rely solely on a .clang-tidy config file by -+ specifying this option as a blank string (``''``). -+ -+The defaults is:: -+ -+ %(default)s -+ -+See also clang-tidy docs for more info.""", -+) -+arg = cli_arg_parser.add_argument( -+ "-V", -+ "--version", -+ default="", -+ help="""The desired version of the clang tools to use. Accepted options are -+strings which can be 8, 9, 10, 11, 12, 13, 14. -+ -+- Set this option to a blank string (``''``) to use the -+ platform's default installed version. -+- This value can also be a path to where the clang tools are -+ installed (if using a custom install location). All paths specified -+ here are converted to absolute. -+ -+Default is """ -+) -+assert arg.help is not None -+arg.help += "a blank string." if not arg.default else f"``{arg.default}``." -+arg = cli_arg_parser.add_argument( -+ "-e", -+ "--extensions", -+ default=["c", "h", "C", "H", "cpp", "hpp", "cc", "hh", "c++", "h++", "cxx", "hxx"], -+ type=lambda i: [ext.strip().lstrip(".") for ext in i.split(",")], -+ help="""The file extensions to analyze. -+This comma-separated string defaults to:: -+ -+ """, -+) -+assert arg.help is not None -+arg.help += ",".join(arg.default) + "\n" -+cli_arg_parser.add_argument( -+ "-r", -+ "--repo-root", -+ default=".", -+ help="""The relative path to the repository root directory. This path is -+relative to the runner's ``GITHUB_WORKSPACE`` environment variable (or -+the current working directory if not using a CI runner). -+ -+The default value is ``%(default)s``""", -+) -+cli_arg_parser.add_argument( -+ "-i", -+ "--ignore", -+ default=".github", -+ help="""Set this option with path(s) to ignore (or not ignore). -+ -+- In the case of multiple paths, you can use ``|`` to separate each path. -+- There is no need to use ``./`` for each entry; a blank string (``''``) -+ represents the repo-root path. -+- This can also have files, but the file's path (relative to -+ the :cli-opt:`repo-root`) has to be specified with the filename. -+- Submodules are automatically ignored. Hidden directories (beginning -+ with a ``.``) are also ignored automatically. -+- Prefix a path with ``!`` to explicitly not ignore it. This can be -+ applied to a submodule's path (if desired) but not hidden directories. -+- Glob patterns are not supported here. All asterisk characters (``*``) -+ are literal.""", -+) -+arg = cli_arg_parser.add_argument( -+ "-l", -+ "--lines-changed-only", -+ default=0, -+ type=lambda a: 2 if a.lower() == "true" else (1 if a.lower() == "diff" else 0), -+ help="""This controls what part of the files are analyzed. -+The following values are accepted: -+ -+- false: All lines in a file are analyzed. -+- true: Only lines in the diff that contain additions are analyzed. -+- diff: All lines in the diff are analyzed (including unchanged -+ lines but not subtractions). -+ -+Defaults to """, -+) -+assert arg.help is not None -+arg.help += f"``{str(bool(arg.default)).lower()}``." -+cli_arg_parser.add_argument( -+ "-f", -+ "--files-changed-only", -+ default="false", -+ type=lambda input: input.lower() == "true", -+ help="""Set this option to false to analyze any source files in the repo. -+This is automatically enabled if -+:cli-opt:`lines-changed-only` is enabled. -+ -+.. note:: -+ The ``GITHUB_TOKEN`` should be supplied when running on a -+ private repository with this option enabled, otherwise the runner -+ does not not have the privilege to list the changed files for an event. -+ -+ See `Authenticating with the GITHUB_TOKEN -+ `_ -+ -+Defaults to ``%(default)s``.""", -+) -+cli_arg_parser.add_argument( -+ "-t", -+ "--thread-comments", -+ default="false", -+ type=lambda input: input.lower() == "true", -+ help="""Set this option to false to disable the use of -+thread comments as feedback. -+ -+.. note:: -+ To use thread comments, the ``GITHUB_TOKEN`` (provided by -+ Github to each repository) must be declared as an environment -+ variable. -+ -+ See `Authenticating with the GITHUB_TOKEN -+ `_ -+ -+.. hint:: -+ If run on a private repository, then this feature is -+ disabled because the GitHub REST API behaves -+ differently for thread comments on a private repository. -+ -+Defaults to ``%(default)s``.""", -+) -+cli_arg_parser.add_argument( -+ "-a", -+ "--file-annotations", -+ default="true", -+ type=lambda input: input.lower() == "true", -+ help="""Set this option to false to disable the use of -+file annotations as feedback. -+ -+Defaults to ``%(default)s``.""", -+) -+ -+ -+def set_exit_code(override: int = None) -> int: -+ """Set the action's exit code. -+ -+ :param override: The number to use when overriding the action's logic. -+ -+ :returns: -+ The exit code that was used. If the ``override`` parameter was not passed, -+ then this value will describe (like a bool value) if any checks failed. -+ """ -+ exit_code = override if override is not None else bool(Globals.OUTPUT) -+ print(f"::set-output name=checks-failed::{exit_code}") -+ return exit_code -+ -+ -+# setup a separate logger for using github log commands -+log_commander = logging.getLogger("LOG COMMANDER") # create a child of our logger obj -+log_commander.setLevel(logging.DEBUG) # be sure that log commands are output -+console_handler = logging.StreamHandler() # Create special stdout stream handler -+console_handler.setFormatter(logging.Formatter("%(message)s")) # no formatted log cmds -+log_commander.addHandler(console_handler) # Use special handler for log_commander -+log_commander.propagate = False -+ -+ -+def start_log_group(name: str) -> None: -+ """Begin a collapsable group of log statements. -+ -+ :param name: The name of the collapsable group -+ """ -+ log_commander.fatal("::group::%s", name) -+ -+ -+def end_log_group() -> None: -+ """End a collapsable group of log statements.""" -+ log_commander.fatal("::endgroup::") -+ -+ -+def is_file_in_list(paths: List[str], file_name: str, prompt: str) -> bool: -+ """Determine if a file is specified in a list of paths and/or filenames. -+ -+ :param paths: A list of specified paths to compare with. This list can contain a -+ specified file, but the file's path must be included as part of the -+ filename. -+ :param file_name: The file's path & name being sought in the ``paths`` list. -+ :param prompt: A debugging prompt to use when the path is found in the list. -+ -+ :returns: -+ -+ - True if ``file_name`` is in the ``paths`` list. -+ - False if ``file_name`` is not in the ``paths`` list. -+ """ -+ for path in paths: -+ result = os.path.commonpath( -+ [PurePath(path).as_posix(), PurePath(file_name).as_posix()] -+ ) -+ if result == path: -+ logger.debug( -+ '"./%s" is %s as specified in the domain "./%s"', -+ file_name, -+ prompt, -+ path, -+ ) -+ return True -+ return False -+ -+ -+def get_list_of_changed_files() -> None: -+ """Fetch the JSON payload of the event's changed files. Sets the -+ :attr:`~cpp_linter.Globals.FILES` attribute.""" -+ start_log_group("Get list of specified source files") -+ files_link = f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/" -+ if GITHUB_EVENT_NAME == "pull_request": -+ files_link += f"pulls/{Globals.EVENT_PAYLOAD['number']}/files" -+ else: -+ if GITHUB_EVENT_NAME != "push": -+ logger.warning( -+ "Triggered on unsupported event '%s'. Behaving like a push event.", -+ GITHUB_EVENT_NAME, -+ ) -+ files_link += f"commits/{GITHUB_SHA}" -+ logger.info("Fetching files list from url: %s", files_link) -+ Globals.response_buffer = requests.get(files_link, headers=API_HEADERS) -+ log_response_msg() -+ if GITHUB_EVENT_NAME == "pull_request": -+ Globals.FILES = Globals.response_buffer.json() -+ else: -+ Globals.FILES = Globals.response_buffer.json()["files"] -+ -+ -+def consolidate_list_to_ranges(just_numbers: List[int]) -> List[List[int]]: -+ """A helper function to `filter_out_non_source_files()` that is only used when -+ extracting the lines from a diff that contain additions.""" -+ result: List[List[int]] = [] -+ for i, n in enumerate(just_numbers): -+ if not i: -+ result.append([n]) -+ elif n - 1 != just_numbers[i - 1]: -+ result[-1].append(just_numbers[i - 1] + 1) -+ result.append([n]) -+ if i == len(just_numbers) - 1: -+ result[-1].append(n + 1) -+ return result -+ -+ -+def filter_out_non_source_files( -+ ext_list: List[str], -+ ignored: List[str], -+ not_ignored: List[str], -+) -> bool: -+ """Exclude undesired files (specified by user input 'extensions'). This filter -+ applies to the event's :attr:`~cpp_linter.Globals.FILES` attribute. -+ -+ :param ext_list: A list of file extensions that are to be examined. -+ :param ignored: A list of paths to explicitly ignore. -+ :param not_ignored: A list of paths to explicitly not ignore. -+ -+ :returns: -+ True if there are files to check. False will invoke a early exit (in -+ `main()`) when no files to be checked. -+ """ -+ files = [] -+ for file in Globals.FILES: -+ if ( -+ PurePath(file["filename"]).suffix.lstrip(".") in ext_list -+ and not file["status"].endswith("removed") -+ and ( -+ not is_file_in_list(ignored, file["filename"], "ignored") -+ or is_file_in_list(not_ignored, file["filename"], "not ignored") -+ ) -+ ): -+ if "patch" in file.keys(): -+ # get diff details for the file's changes -+ # ranges is a list of start/end line numbers shown in the diff -+ ranges: List[List[int]] = [] -+ # additions is a list line numbers in the diff containing additions -+ additions: List[int] = [] -+ line_numb_in_diff: int = 0 -+ for line in cast(str, file["patch"]).splitlines(): -+ if line.startswith("+"): -+ additions.append(line_numb_in_diff) -+ if line.startswith("@@ -"): -+ hunk = line[line.find(" +") + 2 : line.find(" @@")].split(",") -+ start_line, hunk_length = [int(x) for x in hunk] -+ ranges.append([start_line, hunk_length + start_line]) -+ line_numb_in_diff = start_line -+ elif not line.startswith("-"): -+ line_numb_in_diff += 1 -+ file["line_filter"] = dict( -+ diff_chunks=ranges, -+ lines_added=consolidate_list_to_ranges(additions), -+ ) -+ files.append(file) -+ -+ if files: -+ logger.info( -+ "Giving attention to the following files:\n\t%s", -+ "\n\t".join([f["filename"] for f in files]), -+ ) -+ Globals.FILES = files -+ if not IS_ON_RUNNER: # if not executed on a github runner -+ # dump altered json of changed files -+ Path(".changed_files.json").write_text( -+ json.dumps(Globals.FILES, indent=2), -+ encoding="utf-8", -+ ) -+ else: -+ logger.info("No source files need checking!") -+ return False -+ return True -+ -+ -+def verify_files_are_present() -> None: -+ """Download the files if not present. -+ -+ .. hint:: -+ This function assumes the working directory is the root of the invoking -+ repository. If files are not found, then they are downloaded to the working -+ directory. This is bad for files with the same name from different folders. -+ """ -+ for file in Globals.FILES: -+ file_name = Path(file["filename"]) -+ if not file_name.exists(): -+ logger.warning("Could not find %s! Did you checkout the repo?", file_name) -+ logger.info("Downloading file from url: %s", file["raw_url"]) -+ Globals.response_buffer = requests.get(file["raw_url"]) -+ # retain the repo's original structure -+ Path.mkdir(file_name.parent, parents=True, exist_ok=True) -+ file_name.write_text(Globals.response_buffer.text, encoding="utf-8") -+ -+ -+def list_source_files( -+ ext_list: List[str], ignored_paths: List[str], not_ignored: List[str] -+) -> bool: -+ """Make a list of source files to be checked. The resulting list is stored in -+ :attr:`~cpp_linter.Globals.FILES`. -+ -+ :param ext_list: A list of file extensions that should by attended. -+ :param ignored_paths: A list of paths to explicitly ignore. -+ :param not_ignored: A list of paths to explicitly not ignore. -+ -+ :returns: -+ True if there are files to check. False will invoke a early exit (in -+ `main()` when no files to be checked. -+ """ -+ start_log_group("Get list of specified source files") -+ -+ root_path = Path(".") -+ for ext in ext_list: -+ for rel_path in root_path.rglob(f"*.{ext}"): -+ for parent in rel_path.parts[:-1]: -+ if parent.startswith("."): -+ break -+ else: -+ file_path = rel_path.as_posix() -+ logger.debug('"./%s" is a source code file', file_path) -+ if not is_file_in_list( -+ ignored_paths, file_path, "ignored" -+ ) or is_file_in_list(not_ignored, file_path, "not ignored"): -+ Globals.FILES.append(dict(filename=file_path)) -+ -+ if Globals.FILES: -+ logger.info( -+ "Giving attention to the following files:\n\t%s", -+ "\n\t".join([f["filename"] for f in Globals.FILES]), -+ ) -+ else: -+ logger.info("No source files found.") # this might need to be warning -+ return False -+ return True -+ -+ -+def run_clang_tidy( -+ filename: str, -+ file_obj: Dict[str, Any], -+ version: str, -+ checks: str, -+ lines_changed_only: int, -+ database: str, -+ repo_root: str, -+) -> None: -+ """Run clang-tidy on a certain file. -+ -+ :param filename: The name of the local file to run clang-tidy on. -+ :param file_obj: JSON info about the file. -+ :param version: The version of clang-tidy to run. -+ :param checks: The `str` of comma-separated regulate expressions that describe -+ the desired clang-tidy checks to be enabled/configured. -+ :param lines_changed_only: A flag that forces focus on only changes in the event's -+ diff info. -+ :param database: The path to the compilation database. -+ :param repo_root: The path to the repository root folder. -+ """ -+ if checks == "-*": # if all checks are disabled, then clang-tidy is skipped -+ # clear the clang-tidy output file and exit function -+ Path("clang_tidy_report.txt").write_bytes(b"") -+ return -+ filename = PurePath(filename).as_posix() -+ cmds = [ -+ assemble_version_exec("clang-tidy", version), -+ "--export-fixes=clang_tidy_output.yml", -+ ] -+ if checks: -+ cmds.append(f"-checks={checks}") -+ if database: -+ cmds.append("-p") -+ if not PurePath(database).is_absolute(): -+ database = str(Path(RUNNER_WORKSPACE, repo_root, database).resolve()) -+ cmds.append(database) -+ if lines_changed_only: -+ ranges = "diff_chunks" if lines_changed_only == 1 else "lines_added" -+ line_ranges = dict(name=filename, lines=file_obj["line_filter"][ranges]) -+ logger.info("line_filter = %s", json.dumps([line_ranges])) -+ cmds.append(f"--line-filter={json.dumps([line_ranges])}") -+ cmds.append(filename) -+ # clear yml file's content before running clang-tidy -+ Path("clang_tidy_output.yml").write_bytes(b"") -+ logger.info('Running "%s"', " ".join(cmds)) -+ results = subprocess.run(cmds, capture_output=True) -+ Path("clang_tidy_report.txt").write_bytes(results.stdout) -+ logger.debug("Output from clang-tidy:\n%s", results.stdout.decode()) -+ if Path("clang_tidy_output.yml").stat().st_size: -+ parse_tidy_suggestions_yml() # get clang-tidy fixes from yml -+ if results.stderr: -+ logger.debug( -+ "clang-tidy made the following summary:\n%s", results.stderr.decode() -+ ) -+ -+ -+def run_clang_format( -+ filename: str, -+ file_obj: Dict[str, Any], -+ version: str, -+ style: str, -+ lines_changed_only: int, -+) -> None: -+ """Run clang-format on a certain file -+ -+ :param filename: The name of the local file to run clang-format on. -+ :param file_obj: JSON info about the file. -+ :param version: The version of clang-format to run. -+ :param style: The clang-format style rules to adhere. Set this to 'file' to -+ use the relative-most .clang-format configuration file. -+ :param lines_changed_only: A flag that forces focus on only changes in the event's -+ diff info. -+ """ -+ if not style: # if `style` == "" -+ Path("clang_format_output.xml").write_bytes(b"") -+ return # clear any previous output and exit -+ cmds = [ -+ assemble_version_exec("clang-format", version), -+ f"-style={style}", -+ "--output-replacements-xml", -+ ] -+ if lines_changed_only: -+ ranges = "diff_chunks" if lines_changed_only == 1 else "lines_added" -+ for line_range in file_obj["line_filter"][ranges]: -+ cmds.append(f"--lines={line_range[0]}:{line_range[1]}") -+ cmds.append(PurePath(filename).as_posix()) -+ logger.info('Running "%s"', " ".join(cmds)) -+ results = subprocess.run(cmds, capture_output=True) -+ Path("clang_format_output.xml").write_bytes(results.stdout) -+ if results.returncode: -+ logger.debug( -+ "%s raised the following error(s):\n%s", cmds[0], results.stderr.decode() -+ ) -+ -+ -+def create_comment_body( -+ filename: str, -+ file_obj: Dict[str, Any], -+ lines_changed_only: int, -+ tidy_notes: List[TidyNotification], -+): -+ """Create the content for a thread comment about a certain file. -+ This is a helper function to `capture_clang_tools_output()`. -+ -+ :param filename: The file's name (& path). -+ :param file_obj: The file's JSON `dict`. -+ :param lines_changed_only: A flag used to filter the comment based on line changes. -+ :param tidy_notes: A list of cached notifications from clang-tidy. This is used to -+ avoid duplicated content in comment, and it is later used again by -+ `make_annotations()` after `capture_clang_tools_output()` is finished. -+ """ -+ ranges = range_of_changed_lines(file_obj, lines_changed_only) -+ if Path("clang_tidy_report.txt").stat().st_size: -+ parse_tidy_output() # get clang-tidy fixes from stdout -+ comment_output = "" -+ if Globals.PAYLOAD_TIDY: -+ Globals.PAYLOAD_TIDY += "
" -+ for fix in GlobalParser.tidy_notes: -+ if lines_changed_only and fix.line not in ranges: -+ continue -+ comment_output += repr(fix) -+ tidy_notes.append(fix) -+ if comment_output: -+ Globals.PAYLOAD_TIDY += f"
{filename}
\n" -+ Globals.PAYLOAD_TIDY += comment_output -+ GlobalParser.tidy_notes.clear() # empty list to avoid duplicated output -+ -+ if Path("clang_format_output.xml").stat().st_size: -+ parse_format_replacements_xml(PurePath(filename).as_posix()) -+ if GlobalParser.format_advice and GlobalParser.format_advice[-1].replaced_lines: -+ should_comment = lines_changed_only == 0 -+ if not should_comment: -+ for line in [ -+ replacement.line -+ for replacement in GlobalParser.format_advice[-1].replaced_lines -+ ]: -+ if line in ranges: -+ should_comment = True -+ break -+ if should_comment: -+ if not Globals.OUTPUT: -+ Globals.OUTPUT = "\n## :scroll: " -+ Globals.OUTPUT += "Run `clang-format` on the following files\n" -+ Globals.OUTPUT += f"- [ ] {file_obj['filename']}\n" -+ -+ -+def capture_clang_tools_output( -+ version: str, -+ checks: str, -+ style: str, -+ lines_changed_only: int, -+ database: str, -+ repo_root: str, -+): -+ """Execute and capture all output from clang-tidy and clang-format. This aggregates -+ results in the :attr:`~cpp_linter.Globals.OUTPUT`. -+ -+ :param version: The version of clang-tidy to run. -+ :param checks: The `str` of comma-separated regulate expressions that describe -+ the desired clang-tidy checks to be enabled/configured. -+ :param style: The clang-format style rules to adhere. Set this to 'file' to -+ use the relative-most .clang-format configuration file. -+ :param lines_changed_only: A flag that forces focus on only changes in the event's -+ diff info. -+ :param database: The path to the compilation database. -+ :param repo_root: The path to the repository root folder. -+ """ -+ # temporary cache of parsed notifications for use in log commands -+ tidy_notes: List[TidyNotification] = [] -+ for file in Globals.FILES: -+ filename = cast(str, file["filename"]) -+ start_log_group(f"Performing checkup on {filename}") -+ run_clang_tidy( -+ filename, file, version, checks, lines_changed_only, database, repo_root -+ ) -+ run_clang_format(filename, file, version, style, lines_changed_only) -+ end_log_group() -+ -+ create_comment_body(filename, file, lines_changed_only, tidy_notes) -+ -+ if Globals.PAYLOAD_TIDY: -+ if not Globals.OUTPUT: -+ Globals.OUTPUT = "\n" -+ else: -+ Globals.OUTPUT += "\n---\n" -+ Globals.OUTPUT += "## :speech_balloon: Output from `clang-tidy`\n" -+ Globals.OUTPUT += Globals.PAYLOAD_TIDY -+ GlobalParser.tidy_notes = tidy_notes[:] # restore cache of notifications -+ -+ -+def post_push_comment(base_url: str, user_id: int) -> bool: -+ """POST action's results for a push event. -+ -+ :param base_url: The root of the url used to interact with the REST API via -+ `requests`. -+ :param user_id: The user's account ID number. -+ -+ :returns: -+ A bool describing if the linter checks passed. This is used as the action's -+ output value (a soft exit code). -+ """ -+ comments_url = base_url + f"commits/{GITHUB_SHA}/comments" -+ remove_bot_comments(comments_url, user_id) -+ -+ if Globals.OUTPUT: # diff comments are not supported for push events (yet) -+ payload = json.dumps({"body": Globals.OUTPUT}) -+ logger.debug("payload body:\n%s", json.dumps({"body": Globals.OUTPUT})) -+ Globals.response_buffer = requests.post( -+ comments_url, headers=API_HEADERS, data=payload -+ ) -+ logger.info( -+ "Got %d response from POSTing comment", Globals.response_buffer.status_code -+ ) -+ log_response_msg() -+ return bool(Globals.OUTPUT) -+ -+ -+def post_diff_comments(base_url: str, user_id: int) -> bool: -+ """Post comments inside a unified diff (only PRs are supported). -+ -+ :param base_url: The root of the url used to interact with the REST API via -+ `requests`. -+ :param user_id: The user's account ID number. -+ -+ :returns: -+ A bool describing if the linter checks passed. This is used as the action's -+ output value (a soft exit code). -+ """ -+ comments_url = base_url + "pulls/comments/" # for use with comment_id -+ payload = list_diff_comments(2) # only focus on additions in diff -+ logger.info("Posting %d comments", len(payload)) -+ -+ # uncomment the next 3 lines for debug output without posting a comment -+ # for i, comment in enumerate(payload): -+ # logger.debug("comments %d: %s", i, json.dumps(comment, indent=2)) -+ # return -+ -+ # get existing review comments -+ reviews_url = base_url + f'pulls/{Globals.EVENT_PAYLOAD["number"]}/' -+ Globals.response_buffer = requests.get(reviews_url + "comments") -+ existing_comments = json.loads(Globals.response_buffer.text) -+ # filter out comments not made by our bot -+ for index, comment in enumerate(existing_comments): -+ if not comment["body"].startswith(""): -+ del existing_comments[index] -+ -+ # conditionally post comments in the diff -+ for i, body in enumerate(payload): -+ # check if comment is already there -+ already_posted = False -+ comment_id = None -+ for comment in existing_comments: -+ if ( -+ int(comment["user"]["id"]) == user_id -+ and comment["line"] == body["line"] -+ and comment["path"] == body["path"] -+ ): -+ already_posted = True -+ if comment["body"] != body["body"]: -+ comment_id = str(comment["id"]) # use this to update comment -+ else: -+ break -+ if already_posted and comment_id is None: -+ logger.info("comment %d already posted", i) -+ continue # don't bother re-posting the same comment -+ -+ # update ot create a review comment (in the diff) -+ logger.debug("Payload %d body = %s", i, json.dumps(body)) -+ if comment_id is not None: -+ Globals.response_buffer = requests.patch( -+ comments_url + comment_id, -+ headers=API_HEADERS, -+ data=json.dumps({"body": body["body"]}), -+ ) -+ logger.info( -+ "Got %d from PATCHing comment %d (%d)", -+ Globals.response_buffer.status_code, -+ i, -+ comment_id, -+ ) -+ log_response_msg() -+ else: -+ Globals.response_buffer = requests.post( -+ reviews_url + "comments", headers=API_HEADERS, data=json.dumps(body) -+ ) -+ logger.info( -+ "Got %d from POSTing review comment %d", -+ Globals.response_buffer.status_code, -+ i, -+ ) -+ log_response_msg() -+ return bool(payload) -+ -+ -+def post_pr_comment(base_url: str, user_id: int) -> bool: -+ """POST action's results for a push event. -+ -+ :param base_url: The root of the url used to interact with the REST API via -+ `requests`. -+ :param user_id: The user's account ID number. -+ -+ :returns: -+ A bool describing if the linter checks passed. This is used as the action's -+ output value (a soft exit code). -+ """ -+ comments_url = base_url + f'issues/{Globals.EVENT_PAYLOAD["number"]}/comments' -+ remove_bot_comments(comments_url, user_id) -+ payload = "" -+ if Globals.OUTPUT: -+ payload = json.dumps({"body": Globals.OUTPUT}) -+ logger.debug( -+ "payload body:\n%s", json.dumps({"body": Globals.OUTPUT}, indent=2) -+ ) -+ Globals.response_buffer = requests.post( -+ comments_url, headers=API_HEADERS, data=payload -+ ) -+ logger.info("Got %d from POSTing comment", Globals.response_buffer.status_code) -+ log_response_msg() -+ return bool(payload) -+ -+ -+def post_results(use_diff_comments: bool, user_id: int = 41898282): -+ """Post action's results using REST API. -+ -+ :param use_diff_comments: This flag enables making/updating comments in the PR's -+ diff info. -+ :param user_id: The user's account ID number. Defaults to the generic bot's ID. -+ """ -+ if not GITHUB_TOKEN: -+ logger.error("The GITHUB_TOKEN is required!") -+ sys.exit(set_exit_code(1)) -+ -+ base_url = f"{GITHUB_API_URL}/repos/{GITHUB_REPOSITORY}/" -+ checks_passed = True -+ if GITHUB_EVENT_NAME == "pull_request": -+ checks_passed = post_pr_comment(base_url, user_id) -+ if use_diff_comments: -+ checks_passed = post_diff_comments(base_url, user_id) -+ elif GITHUB_EVENT_NAME == "push": -+ checks_passed = post_push_comment(base_url, user_id) -+ set_exit_code(1 if checks_passed else 0) -+ -+ -+def make_annotations( -+ style: str, file_annotations: bool, lines_changed_only: int -+) -> bool: -+ """Use github log commands to make annotations from clang-format and -+ clang-tidy output. -+ -+ :param style: The chosen code style guidelines. The value 'file' is replaced with -+ 'custom style'. -+ -+ :returns: -+ A boolean describing if any annotations were made. -+ """ -+ count = 0 -+ files = ( -+ Globals.FILES -+ if GITHUB_EVENT_NAME == "pull_request" or isinstance(Globals.FILES, list) -+ else cast(Dict[str, Any], Globals.FILES)["files"] -+ ) -+ for advice, file in zip(GlobalParser.format_advice, files): -+ line_filter = range_of_changed_lines(file, lines_changed_only) -+ if advice.replaced_lines: -+ if file_annotations: -+ output = advice.log_command(style, line_filter) -+ if output is not None: -+ log_commander.info(output) -+ count += 1 -+ for note in GlobalParser.tidy_notes: -+ if lines_changed_only: -+ filename = note.filename.replace("\\", "/") -+ line_filter = [] -+ for file in files: -+ if filename == file["filename"]: -+ line_filter = range_of_changed_lines(file, lines_changed_only) -+ break -+ else: -+ continue -+ if note.line in line_filter: -+ count += 1 -+ log_commander.info(note.log_command()) -+ else: -+ count += 1 -+ log_commander.info(note.log_command()) -+ logger.info("Created %d annotations", count) -+ return bool(count) -+ -+ -+def parse_ignore_option(paths: str) -> Tuple[List[str], List[str]]: -+ """Parse a given string of paths (separated by a ``|``) into ``ignored`` and -+ ``not_ignored`` lists of strings. -+ -+ :param paths: This argument conforms to the input value of CLI arg -+ :cli-opt:`ignore`. -+ -+ :returns: -+ Returns a tuple of lists in which each list is a set of strings. -+ -+ - index 0 is the ``ignored`` list -+ - index 1 is the ``not_ignored`` list -+ """ -+ ignored, not_ignored = ([], []) -+ -+ for path in paths.split("|"): -+ is_included = path.startswith("!") -+ if path.startswith("!./" if is_included else "./"): -+ path = path.replace("./", "", 1) # relative dir is assumed -+ path = path.strip() # strip leading/trailing spaces -+ if is_included: -+ not_ignored.append(path[1:]) # strip leading `!` -+ else: -+ ignored.append(path) -+ -+ # auto detect submodules -+ gitmodules = Path(".gitmodules") -+ if gitmodules.exists(): -+ submodules = configparser.ConfigParser() -+ submodules.read(gitmodules.resolve().as_posix()) -+ for module in submodules.sections(): -+ path = submodules[module]["path"] -+ if path not in not_ignored: -+ logger.info("Appending submodule to ignored paths: %s", path) -+ ignored.append(path) -+ -+ if ignored: -+ logger.info( -+ "Ignoring the following paths/files:\n\t./%s", -+ "\n\t./".join(f for f in ignored), -+ ) -+ if not_ignored: -+ logger.info( -+ "Not ignoring the following paths/files:\n\t./%s", -+ "\n\t./".join(f for f in not_ignored), -+ ) -+ return (ignored, not_ignored) -+ -+ -+def main(): -+ """The main script.""" -+ -+ # The parsed CLI args -+ args = cli_arg_parser.parse_args() -+ -+ # force files-changed-only to reflect value of lines-changed-only -+ if args.lines_changed_only: -+ args.files_changed_only = True -+ -+ # set logging verbosity -+ logger.setLevel(int(args.verbosity)) -+ -+ # prepare ignored paths list -+ ignored, not_ignored = parse_ignore_option(args.ignore) -+ -+ logger.info("processing %s event", GITHUB_EVENT_NAME) -+ -+ # change working directory -+ os.chdir(args.repo_root) -+ -+ if GITHUB_EVENT_PATH: -+ # load event's json info about the workflow run -+ Globals.EVENT_PAYLOAD = json.loads( -+ Path(GITHUB_EVENT_PATH).read_text(encoding="utf-8") -+ ) -+ if logger.getEffectiveLevel() <= logging.DEBUG: -+ start_log_group("Event json from the runner") -+ logger.debug(json.dumps(Globals.EVENT_PAYLOAD)) -+ end_log_group() -+ -+ exit_early = False -+ if args.files_changed_only: -+ get_list_of_changed_files() -+ exit_early = not filter_out_non_source_files( -+ args.extensions, -+ ignored, -+ not_ignored, -+ ) -+ if not exit_early: -+ verify_files_are_present() -+ else: -+ exit_early = not list_source_files(args.extensions, ignored, not_ignored) -+ end_log_group() -+ if exit_early: -+ sys.exit(set_exit_code(0)) -+ -+ capture_clang_tools_output( -+ args.version, -+ args.tidy_checks, -+ args.style, -+ args.lines_changed_only, -+ args.database, -+ args.repo_root, -+ ) -+ -+ start_log_group("Posting comment(s)") -+ thread_comments_allowed = True -+ if GITHUB_EVENT_PATH and "private" in Globals.EVENT_PAYLOAD["repository"]: -+ thread_comments_allowed = ( -+ Globals.EVENT_PAYLOAD["repository"]["private"] is not True -+ ) -+ if args.thread_comments and thread_comments_allowed: -+ post_results(False) # False is hard-coded to disable diff comments. -+ set_exit_code( -+ int( -+ make_annotations(args.style, args.file_annotations, args.lines_changed_only) -+ ) -+ ) -+ end_log_group() -+ -+ -+if __name__ == "__main__": -+ main() -diff --git a/cpp_linter/thread_comments.py b/cpp_linter/thread_comments.py -new file mode 100644 -index 0000000..1d0699a ---- /dev/null -+++ b/cpp_linter/thread_comments.py -@@ -0,0 +1,269 @@ -+"""A module to house the various functions for traversing/adjusting comments""" -+from typing import Union, cast, List, Optional, Dict, Any -+import json -+from pathlib import Path -+import requests -+from . import ( -+ Globals, -+ GlobalParser, -+ logger, -+ API_HEADERS, -+ GITHUB_SHA, -+ log_response_msg, -+ range_of_changed_lines, -+) -+ -+ -+def remove_bot_comments(comments_url: str, user_id: int): -+ """Traverse the list of comments made by a specific user -+ and remove all. -+ -+ :param comments_url: The URL used to fetch the comments. -+ :param user_id: The user's account id number. -+ """ -+ logger.info("comments_url: %s", comments_url) -+ Globals.response_buffer = requests.get(comments_url) -+ if not log_response_msg(): -+ return # error getting comments for the thread; stop here -+ comments = Globals.response_buffer.json() -+ for comment in comments: -+ # only search for comments from the user's ID and -+ # whose comment body begins with a specific html comment -+ if ( -+ int(comment["user"]["id"]) == user_id -+ # the specific html comment is our action's name -+ and comment["body"].startswith("") -+ ): -+ # remove other outdated comments but don't remove the last comment -+ Globals.response_buffer = requests.delete( -+ comment["url"], -+ headers=API_HEADERS, -+ ) -+ logger.info( -+ "Got %d from DELETE %s", -+ Globals.response_buffer.status_code, -+ comment["url"][comment["url"].find(".com") + 4 :], -+ ) -+ log_response_msg() -+ logger.debug( -+ "comment id %d from user %s (%d)", -+ comment["id"], -+ comment["user"]["login"], -+ comment["user"]["id"], -+ ) -+ with open("comments.json", "w", encoding="utf-8") as json_comments: -+ json.dump(comments, json_comments, indent=4) -+ -+ -+def aggregate_tidy_advice(lines_changed_only: int) -> List[Dict[str, Any]]: -+ """Aggregate a list of json contents representing advice from clang-tidy -+ suggestions. -+ -+ :param lines_changed_only: A flag indicating the focus of the advice that -+ should be headed. -+ """ -+ results = [] -+ for fixit, file in zip(GlobalParser.tidy_advice, Globals.FILES): -+ for diag in fixit.diagnostics: -+ ranges = range_of_changed_lines(file, lines_changed_only) -+ if lines_changed_only and diag.line not in ranges: -+ continue -+ -+ # base body of comment -+ body = "\n## :speech_balloon: Clang-tidy\n**" -+ body += diag.name + "**\n>" + diag.message -+ -+ # get original code -+ filename = Path(cast(str, file["filename"])) -+ # the list of lines in a file -+ lines = filename.read_text(encoding="utf-8").splitlines() -+ -+ # aggregate clang-tidy advice -+ suggestion = "\n```suggestion\n" -+ is_multiline_fix = False -+ fix_lines: List[int] = [] # a list of line numbers for the suggested fixes -+ line = "" # the line that concerns the fix/comment -+ for i, tidy_fix in enumerate(diag.replacements): -+ line = lines[tidy_fix.line - 1] -+ if not fix_lines: -+ fix_lines.append(tidy_fix.line) -+ elif tidy_fix.line not in fix_lines: -+ is_multiline_fix = True -+ break -+ if i: # if this isn't the first tidy_fix for the same line -+ last_fix = diag.replacements[i - 1] -+ suggestion += ( -+ line[last_fix.cols + last_fix.null_len - 1 : tidy_fix.cols - 1] -+ + tidy_fix.text.decode() -+ ) -+ else: -+ suggestion += line[: tidy_fix.cols - 1] + tidy_fix.text.decode() -+ if not is_multiline_fix and diag.replacements: -+ # complete suggestion with original src code and closing md fence -+ last_fix = diag.replacements[len(diag.replacements) - 1] -+ suggestion += line[last_fix.cols + last_fix.null_len - 1 : -1] + "\n```" -+ body += suggestion -+ -+ results.append( -+ dict( -+ body=body, -+ commit_id=GITHUB_SHA, -+ line=diag.line, -+ path=fixit.filename, -+ side="RIGHT", -+ ) -+ ) -+ return results -+ -+ -+def aggregate_format_advice(lines_changed_only: int) -> List[Dict[str, Any]]: -+ """Aggregate a list of json contents representing advice from clang-format -+ suggestions.""" -+ results = [] -+ for fmt_advice, file in zip(GlobalParser.format_advice, Globals.FILES): -+ -+ # get original code -+ filename = Path(file["filename"]) -+ # the list of lines from the src file -+ lines = filename.read_text(encoding="utf-8").splitlines() -+ -+ # aggregate clang-format suggestion -+ line = "" # the line that concerns the fix -+ for fixed_line in fmt_advice.replaced_lines: -+ # clang-format can include advice that starts/ends outside the diff's domain -+ ranges = range_of_changed_lines(file, lines_changed_only) -+ if lines_changed_only and fixed_line.line not in ranges: -+ continue # line is out of scope for diff, so skip this fix -+ -+ # assemble the suggestion -+ body = "## :scroll: clang-format advice\n```suggestion\n" -+ line = lines[fixed_line.line - 1] -+ # logger.debug("%d >>> %s", fixed_line.line, line[:-1]) -+ for fix_index, line_fix in enumerate(fixed_line.replacements): -+ # logger.debug( -+ # "%s >>> %s", repr(line_fix), line_fix.text.encode("utf-8") -+ # ) -+ if fix_index: -+ last_fix = fixed_line.replacements[fix_index - 1] -+ body += line[ -+ last_fix.cols + last_fix.null_len - 1 : line_fix.cols - 1 -+ ] -+ body += line_fix.text -+ else: -+ body += line[: line_fix.cols - 1] + line_fix.text -+ # complete suggestion with original src code and closing md fence -+ last_fix = fixed_line.replacements[-1] -+ body += line[last_fix.cols + last_fix.null_len - 1 : -1] + "\n```" -+ # logger.debug("body <<< %s", body) -+ -+ # create a suggestion from clang-format advice -+ results.append( -+ dict( -+ body=body, -+ commit_id=GITHUB_SHA, -+ line=fixed_line.line, -+ path=fmt_advice.filename, -+ side="RIGHT", -+ ) -+ ) -+ return results -+ -+ -+def concatenate_comments( -+ tidy_advice: list, format_advice: list -+) -> List[Dict[str, Union[str, int]]]: -+ """Concatenate comments made to the same line of the same file. -+ -+ :param tidy_advice: Pass the output from `aggregate_tidy_advice()` here. -+ :param format_advice: Pass the output from `aggregate_format_advice()` here. -+ """ -+ # traverse comments from clang-format -+ for index, comment_body in enumerate(format_advice): -+ # check for comments from clang-tidy on the same line -+ comment_index = None -+ for i, payload in enumerate(tidy_advice): -+ if ( -+ payload["line"] == comment_body["line"] -+ and payload["path"] == comment_body["path"] -+ ): -+ comment_index = i # mark this comment for concatenation -+ break -+ if comment_index is not None: -+ # append clang-format advice to clang-tidy output/suggestion -+ tidy_advice[comment_index]["body"] += "\n" + comment_body["body"] -+ del format_advice[index] # remove duplicate comment -+ return tidy_advice + format_advice -+ -+ -+def list_diff_comments(lines_changed_only: int) -> List[Dict[str, Union[str, int]]]: -+ """Aggregate list of comments for use in the event's diff. This function assumes -+ that the CLI option ``--lines_changed_only`` is set to True. -+ -+ :param lines_changed_only: A flag indicating the focus of the advice that -+ should be headed. -+ -+ :returns: -+ A list of comments (each element as json content). -+ """ -+ return concatenate_comments( -+ aggregate_tidy_advice(lines_changed_only), -+ aggregate_format_advice(lines_changed_only), -+ ) -+ -+ -+def get_review_id(reviews_url: str, user_id: int) -> Optional[int]: -+ """Dismiss all stale reviews (only the ones made by our bot). -+ -+ :param reviews_url: The URL used to fetch the review comments. -+ :param user_id: The user's account id number. -+ -+ :returns: -+ The ID number of the review created by the action's generic bot. -+ """ -+ logger.info(" review_url: %s", reviews_url) -+ Globals.response_buffer = requests.get(reviews_url) -+ review_id = find_review(json.loads(Globals.response_buffer.text), user_id) -+ if review_id is None: # create a PR review -+ Globals.response_buffer = requests.post( -+ reviews_url, -+ headers=API_HEADERS, -+ data=json.dumps( -+ { -+ "body": "\n" -+ "CPP Linter Action found no problems", -+ "event": "COMMENTED", -+ } -+ ), -+ ) -+ logger.info( -+ "Got %d from POSTing new(/temp) PR review", -+ Globals.response_buffer.status_code, -+ ) -+ Globals.response_buffer = requests.get(reviews_url) -+ if Globals.response_buffer.status_code != 200 and log_response_msg(): -+ raise RuntimeError("could not create a review for comments") -+ reviews = json.loads(Globals.response_buffer.text) -+ reviews.reverse() # traverse the list in reverse -+ review_id = find_review(reviews, user_id) -+ return review_id -+ -+ -+def find_review(reviews: dict, user_id: int) -> Optional[int]: -+ """Find a review created by a certain user ID. -+ -+ :param reviews: the JSON object fetched via GIT REST API. -+ :param user_id: The user account's ID number -+ -+ :returns: -+ An ID that corresponds to the specified ``user_id``. -+ """ -+ review_id = None -+ for review in reviews: -+ if int(review["user"]["id"]) == user_id and review["body"].startswith( -+ "" -+ ): -+ review_id = int(review["id"]) -+ break # there will only be 1 review from this action, so break when found -+ -+ logger.info(" review_id: %d", review_id) -+ return review_id -diff --git a/docs/API-Reference/cpp_linter.clang_format_xml.rst b/docs/API-Reference/cpp_linter.clang_format_xml.rst -new file mode 100644 -index 0000000..6011fdf ---- /dev/null -+++ b/docs/API-Reference/cpp_linter.clang_format_xml.rst -@@ -0,0 +1,10 @@ -+clang_format_xml module -+======================= -+ -+.. admonition:: Info -+ :class: info -+ -+ This API is experimental and not actually used in production. -+ -+.. automodule:: cpp_linter.clang_format_xml -+ :members: -diff --git a/docs/API-Reference/cpp_linter.clang_tidy.rst b/docs/API-Reference/cpp_linter.clang_tidy.rst -new file mode 100644 -index 0000000..9aecade ---- /dev/null -+++ b/docs/API-Reference/cpp_linter.clang_tidy.rst -@@ -0,0 +1,5 @@ -+clang_tidy module -+================= -+ -+.. automodule:: cpp_linter.clang_tidy -+ :members: -diff --git a/docs/API-Reference/cpp_linter.clang_tidy_yml.rst b/docs/API-Reference/cpp_linter.clang_tidy_yml.rst -new file mode 100644 -index 0000000..695a7d0 ---- /dev/null -+++ b/docs/API-Reference/cpp_linter.clang_tidy_yml.rst -@@ -0,0 +1,10 @@ -+clang_tidy_yml module -+===================== -+ -+.. admonition:: Info -+ :class: info -+ -+ This API is experimental and not actually used in production. -+ -+.. automodule:: cpp_linter.clang_tidy_yml -+ :members: -diff --git a/docs/API-Reference/cpp_linter.rst b/docs/API-Reference/cpp_linter.rst -new file mode 100644 -index 0000000..a2ba1f3 ---- /dev/null -+++ b/docs/API-Reference/cpp_linter.rst -@@ -0,0 +1,5 @@ -+Base module -+=========== -+ -+.. automodule:: cpp_linter -+ :members: -diff --git a/docs/API-Reference/cpp_linter.run.rst b/docs/API-Reference/cpp_linter.run.rst -new file mode 100644 -index 0000000..b2c8159 ---- /dev/null -+++ b/docs/API-Reference/cpp_linter.run.rst -@@ -0,0 +1,5 @@ -+Run module -+========== -+ -+.. automodule:: cpp_linter.run -+ :members: -diff --git a/docs/API-Reference/cpp_linter.thread_comments.rst b/docs/API-Reference/cpp_linter.thread_comments.rst -new file mode 100644 -index 0000000..ceb3005 ---- /dev/null -+++ b/docs/API-Reference/cpp_linter.thread_comments.rst -@@ -0,0 +1,5 @@ -+thread_comments module -+====================== -+ -+.. automodule:: cpp_linter.thread_comments -+ :members: -diff --git a/docs/_static/extra_css.css b/docs/_static/extra_css.css -new file mode 100644 -index 0000000..8e2a15e ---- /dev/null -+++ b/docs/_static/extra_css.css -@@ -0,0 +1,10 @@ -+tbody .stub, -+thead { -+ background-color: var(--md-accent-bg-color--light); -+ color: var(--md-default-bg-color); -+} -+ -+.md-header, -+.md-nav--primary .md-nav__title[for="__drawer"] { -+ background-color: #4051b5; -+} -diff --git a/docs/_static/favicon.ico b/docs/_static/favicon.ico -new file mode 100644 -index 0000000..c5de55c -Binary files /dev/null and b/docs/_static/favicon.ico differ -diff --git a/docs/_static/logo.png b/docs/_static/logo.png -new file mode 100644 -index 0000000..9c3e4db -Binary files /dev/null and b/docs/_static/logo.png differ -diff --git a/docs/building_docs.rst b/docs/building_docs.rst -new file mode 100644 -index 0000000..069f108 ---- /dev/null -+++ b/docs/building_docs.rst -@@ -0,0 +1,21 @@ -+How to build the docs -+===================== -+ -+From the root directory of the repository, do the following to steps -+ -+1. Install docs' dependencies -+ -+ .. code-block:: text -+ -+ pip install -r docs/requirements.txt -+ -+ On Linux, you may need to use ``pip3`` instead. -+ -+2. Build the docs -+ -+ .. code-block:: text -+ -+ sphinx-build docs docs/_build/html -+ -+ Browse the files in docs/_build/html with your internet browser to see the rendered -+ output. -diff --git a/docs/conf.py b/docs/conf.py -new file mode 100644 -index 0000000..b6d772e ---- /dev/null -+++ b/docs/conf.py -@@ -0,0 +1,138 @@ -+# pylint: disable=all -+# Configuration file for the Sphinx documentation builder. -+# -+# For the full list of built-in configuration values, see the documentation: -+# https://www.sphinx-doc.org/en/master/usage/configuration.html -+ -+import re -+from pathlib import Path -+import io -+from docutils.nodes import Node -+from sphinx import addnodes -+from sphinx.application import Sphinx -+from sphinx.environment import BuildEnvironment -+from cpp_linter.run import cli_arg_parser -+ -+# -- Project information ----------------------------------------------------- -+# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -+project = "cpp-linter" -+copyright = "2022, 2bndy5" -+author = "2bndy5" -+release = "2.0.0" -+ -+# -- General configuration --------------------------------------------------- -+# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -+extensions = [ -+ "sphinx_immaterial", -+ "sphinx.ext.autodoc", -+ "sphinx.ext.intersphinx", -+ "sphinx.ext.viewcode", -+] -+ -+intersphinx_mapping = { -+ "python": ("https://docs.python.org/3", None), -+ "requests": ("https://requests.readthedocs.io/en/latest/", None), -+} -+ -+templates_path = ["_templates"] -+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] -+ -+default_role = "any" -+ -+# -- Options for HTML output ------------------------------------------------- -+# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -+ -+html_theme = "sphinx_immaterial" -+html_static_path = ["_static"] -+html_logo = "_static/logo.png" -+html_favicon = "_static/favicon.ico" -+html_css_files = ["extra_css.css"] -+html_title = "cpp-linter" -+ -+html_theme_options = { -+ "repo_url": "https://github.com/cpp-linter/cpp-linter", -+ "repo_name": "cpp-linter", -+ "repo_type": "github", -+ "palette": [ -+ { -+ "media": "(prefers-color-scheme: light)", -+ "scheme": "default", -+ "primary": "light-blue", -+ "accent": "deep-purple", -+ "toggle": { -+ "icon": "material/lightbulb-outline", -+ "name": "Switch to dark mode", -+ }, -+ }, -+ { -+ "media": "(prefers-color-scheme: dark)", -+ "scheme": "slate", -+ "primary": "light-blue", -+ "accent": "deep-purple", -+ "toggle": { -+ "icon": "material/lightbulb", -+ "name": "Switch to light mode", -+ }, -+ }, -+ ], -+ "features": [ -+ "navigation.top", -+ "navigation.tabs", -+ "navigation.tabs.sticky", -+ "toc.sticky", -+ "toc.follow", -+ "search.share", -+ ], -+} -+ -+object_description_options = [ -+ ("py:parameter", dict(include_in_toc=False)), -+] -+ -+# -- Parse CLI args from `-h` output ------------------------------------- -+ -+ -+def parse_cli_option(env: BuildEnvironment, sig: str, sig_node: Node): -+ """parse the given signature of a CLI option and -+ return the docutil nodes accordingly.""" -+ opt_names = sig.split(", ") -+ sig_node["is_multiline"] = True -+ for i, opt_name in enumerate(opt_names): -+ name = addnodes.desc_signature_line("", "--" if i else opt_name) -+ if not i: -+ name["add_permalink"] = True -+ else: -+ name += addnodes.desc_name(opt_name, opt_name.lstrip("-")) -+ sig_node += name -+ # print(sig_node.pformat()) -+ return opt_names[-1].lstrip("-") -+ -+ -+def setup(app: Sphinx): -+ """Generate a doc from the executable script's ``--help`` output.""" -+ app.add_object_type( -+ "cli-opt", -+ "cli-opt", -+ objname="Command Line Interface option", -+ indextemplate="pair: %s; Command Line Interface option", -+ parse_node=parse_cli_option, -+ ) -+ -+ with io.StringIO() as help_out: -+ cli_arg_parser.print_help(help_out) -+ output = help_out.getvalue() -+ first_line = re.search(r"^options:\s*\n", output, re.MULTILINE) -+ if first_line is None: -+ raise OSError("unrecognized output from `cpp-linter -h`") -+ output = output[first_line.end(0) :] -+ doc = "Command Line Interface Options\n==============================\n\n" -+ CLI_OPT_NAME = re.compile(r"^\s*(\-\w)\s?[A-Z_]*,\s(\-\-.*?)\s") -+ for line in output.splitlines(): -+ match = CLI_OPT_NAME.search(line) -+ if match is not None: -+ # print(match.groups()) -+ doc += "\n.. cli-opt:: " + ", ".join(match.groups()) + "\n\n" -+ doc += line + "\n" -+ cli_doc = Path(app.srcdir, "cli_args.rst") -+ cli_doc.unlink(missing_ok=True) -+ cli_doc.write_text(doc) -diff --git a/docs/index.rst b/docs/index.rst -new file mode 100644 -index 0000000..f723b71 ---- /dev/null -+++ b/docs/index.rst -@@ -0,0 +1,29 @@ -+.. include:: ../README.rst -+ -+.. toctree:: -+ :hidden: -+ -+ self -+ building_docs -+ -+.. toctree:: -+ :hidden: -+ -+ cli_args -+ -+.. toctree:: -+ :hidden: -+ :caption: API Reference -+ -+ API-Reference/cpp_linter -+ API-Reference/cpp_linter.run -+ API-Reference/cpp_linter.clang_tidy -+ API-Reference/cpp_linter.clang_tidy_yml -+ API-Reference/cpp_linter.clang_format_xml -+ API-Reference/cpp_linter.thread_comments -+ -+Indices and tables -+================== -+ -+* :ref:`genindex` -+* :ref:`modindex` -diff --git a/docs/requirements.txt b/docs/requirements.txt -new file mode 100644 -index 0000000..d078f24 ---- /dev/null -+++ b/docs/requirements.txt -@@ -0,0 +1 @@ -+sphinx-immaterial -diff --git a/pyproject.toml b/pyproject.toml -new file mode 100644 -index 0000000..981b64c ---- /dev/null -+++ b/pyproject.toml -@@ -0,0 +1,616 @@ -+[build-system] -+requires = ["setuptools>=45", "setuptools-scm"] -+build-backend = "setuptools.build_meta" -+ -+[project] -+name = "cpp-linter" -+description = "Run clang-format and clang-tidy on a batch of files." -+readme = "README.md" -+keywords = ["clang", "clang-tools", "linter", "clang-tidy", "clang-format"] -+license = {text = "MIT License"} -+authors = [ -+ { name = "Brendan Doherty", email = "2bndy5@gmail.com" }, -+ { name = "Peter Shen", email = "xianpeng.shen@gmail.com" }, -+] -+dependencies = [ -+ "requests", -+ "pyyaml", -+] -+classifiers = [ -+ # https://pypi.org/pypi?%3Aaction=list_classifiers -+ "Development Status :: 5 - Production/Stable", -+ "License :: OSI Approved :: MIT License", -+ "Intended Audience :: Developers", -+ "Intended Audience :: System Administrators", -+ "Intended Audience :: Information Technology", -+ "Natural Language :: English", -+ "Operating System :: Microsoft :: Windows", -+ "Operating System :: POSIX :: Linux", -+ "Operating System :: MacOS", -+ "Programming Language :: Python :: 3", -+ "Topic :: Software Development :: Build Tools", -+] -+dynamic = ["version"] -+ -+[project.scripts] -+cpp-linter = "cpp_linter.run:main" -+ -+[project.urls] -+source = "https://github.com/cpp-linter/cpp-linter" -+tracker = "https://github.com/cpp-linter/cpp-linter/issues" -+ -+# ... other project metadata fields as specified in: -+# https://packaging.python.org/en/latest/specifications/declaring-project-metadata/ -+ -+[tool.setuptools] -+zip-safe = false -+packages = ["cpp_linter"] -+ -+[tool.setuptools_scm] -+# It would be nice to include the commit hash in the version, but that -+# can't be done in a PEP 440-compatible way. -+version_scheme= "no-guess-dev" -+# Test PyPI does not support local versions. -+local_scheme = "no-local-version" -+fallback_version = "0.0.0" -+ -+[tool.mypy] -+show_error_codes = true -+show_column_numbers = true -+ -+[tool.pytest.ini_options] -+minversion = "6.0" -+addopts = "-vv" -+testpaths = ["tests"] -+ -+[tool.coverage] -+[tool.coverage.run] -+dynamic_context = "test_function" -+omit = [ -+ # don't include tests in coverage -+ "tests/*", -+] -+ -+[tool.coverage.json] -+pretty_print = true -+ -+[tool.coverage.html] -+show_contexts = true -+ -+[tool.coverage.report] -+# Regexes for lines to exclude from consideration -+exclude_lines = [ -+ # Have to re-enable the standard pragma -+ "pragma: no cover", -+ # Don\'t complain about missing debug-only code: -+ "def __repr__", -+ # the point of unit tests is to test parts of main() -+ "def main", -+ # ignore any branch that makes the module executable -+ 'if __name__ == "__main__"', -+ # ignore branches specific to type checking -+ "if TYPE_CHECKING", -+ # ignore the local secific debug statement related to not having rich installed -+ "if not FOUND_RICH_LIB", -+] -+ -+[tool.pylint.main] -+# Analyse import fallback blocks. This can be used to support both Python 2 and 3 -+# compatible code, which means that the block might have code that exists only in -+# one or another interpreter, leading to false positives when analysed. -+# analyse-fallback-blocks = -+ -+# Always return a 0 (non-error) status code, even if lint errors are found. This -+# is primarily useful in continuous integration scripts. -+# exit-zero = -+ -+# A comma-separated list of package or module names from where C extensions may -+# be loaded. Extensions are loading into the active Python interpreter and may -+# run arbitrary code. -+# extension-pkg-allow-list = -+ -+# A comma-separated list of package or module names from where C extensions may -+# be loaded. Extensions are loading into the active Python interpreter and may -+# run arbitrary code. (This is an alternative name to extension-pkg-allow-list -+# for backward compatibility.) -+# extension-pkg-whitelist = -+ -+# Return non-zero exit code if any of these messages/categories are detected, -+# even if score is above --fail-under value. Syntax same as enable. Messages -+# specified are enabled, while categories only check already-enabled messages. -+# fail-on = -+ -+# Specify a score threshold to be exceeded before program exits with error. -+fail-under = 10 -+ -+# Interpret the stdin as a python script, whose filename needs to be passed as -+# the module_or_package argument. -+# from-stdin = -+ -+# Files or directories to be skipped. They should be base names, not paths. -+ignore = ["CVS"] -+ -+# Add files or directories matching the regex patterns to the ignore-list. The -+# regex matches against paths and can be in Posix or Windows format. -+# ignore-paths = -+ -+# Files or directories matching the regex patterns are skipped. The regex matches -+# against base names, not paths. The default value ignores Emacs file locks -+# ignore-patterns = -+ -+# List of module names for which member attributes should not be checked (useful -+# for modules/projects where namespaces are manipulated during runtime and thus -+# existing member attributes cannot be deduced by static analysis). It supports -+# qualified module names, as well as Unix pattern matching. -+# ignored-modules = -+ -+# Python code to execute, usually for sys.path manipulation such as -+# pygtk.require(). -+# init-hook = -+ -+# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -+# number of processors available to use. -+jobs = 2 -+ -+# Control the amount of potential inferred values when inferring a single object. -+# This can help the performance when dealing with large functions or complex, -+# nested conditions. -+limit-inference-results = 100 -+ -+# List of plugins (as comma separated values of python module names) to load, -+# usually to register additional checkers. -+# load-plugins = -+ -+# Pickle collected data for later comparisons. -+persistent = true -+ -+# Minimum Python version to use for version dependent checks. Will default to the -+# version used to run pylint. -+py-version = "3.10" -+ -+# Discover python modules and packages in the file system subtree. -+# recursive = -+ -+# When enabled, pylint would attempt to guess common misconfiguration and emit -+# user-friendly hints instead of false-positive error messages. -+suggestion-mode = true -+ -+# Allow loading of arbitrary C extensions. Extensions are imported into the -+# active Python interpreter and may run arbitrary code. -+# unsafe-load-any-extension = -+ -+[tool.pylint.basic] -+# Naming style matching correct argument names. -+argument-naming-style = "snake_case" -+ -+# Regular expression matching correct argument names. Overrides argument-naming- -+# style. If left empty, argument names will be checked with the set naming style. -+argument-rgx = "(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$" -+ -+# Naming style matching correct attribute names. -+attr-naming-style = "snake_case" -+ -+# Regular expression matching correct attribute names. Overrides attr-naming- -+# style. If left empty, attribute names will be checked with the set naming -+# style. -+attr-rgx = "(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$" -+ -+# Bad variable names which should always be refused, separated by a comma. -+bad-names = ["foo", "bar", "baz", "toto", "tutu", "tata"] -+ -+# Bad variable names regexes, separated by a comma. If names match any regex, -+# they will always be refused -+# bad-names-rgxs = -+ -+# Naming style matching correct class attribute names. -+class-attribute-naming-style = "any" -+ -+# Regular expression matching correct class attribute names. Overrides class- -+# attribute-naming-style. If left empty, class attribute names will be checked -+# with the set naming style. -+class-attribute-rgx = "([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$" -+ -+# Naming style matching correct class constant names. -+class-const-naming-style = "UPPER_CASE" -+ -+# Regular expression matching correct class constant names. Overrides class- -+# const-naming-style. If left empty, class constant names will be checked with -+# the set naming style. -+# class-const-rgx = -+ -+# Naming style matching correct class names. -+class-naming-style = "PascalCase" -+ -+# Regular expression matching correct class names. Overrides class-naming-style. -+# If left empty, class names will be checked with the set naming style. -+class-rgx = "[A-Z_][a-zA-Z0-9_]+$" -+ -+# Naming style matching correct constant names. -+const-naming-style = "UPPER_CASE" -+ -+# Regular expression matching correct constant names. Overrides const-naming- -+# style. If left empty, constant names will be checked with the set naming style. -+const-rgx = "(([A-Z_][A-Z0-9_]*)|(__.*__))$" -+ -+# Minimum line length for functions/classes that require docstrings, shorter ones -+# are exempt. -+docstring-min-length = -1 -+ -+# Naming style matching correct function names. -+function-naming-style = "snake_case" -+ -+# Regular expression matching correct function names. Overrides function-naming- -+# style. If left empty, function names will be checked with the set naming style. -+function-rgx = "(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$" -+ -+# Good variable names which should always be accepted, separated by a comma. -+good-names = ["r", "g", "b", "w", "i", "j", "k", "n", "x", "y", "z", "ex", "ok", "Run", "_"] -+ -+# Good variable names regexes, separated by a comma. If names match any regex, -+# they will always be accepted -+# good-names-rgxs = -+ -+# Include a hint for the correct naming format with invalid-name. -+# include-naming-hint = -+ -+# Naming style matching correct inline iteration names. -+inlinevar-naming-style = "any" -+ -+# Regular expression matching correct inline iteration names. Overrides -+# inlinevar-naming-style. If left empty, inline iteration names will be checked -+# with the set naming style. -+inlinevar-rgx = "[A-Za-z_][A-Za-z0-9_]*$" -+ -+# Naming style matching correct method names. -+method-naming-style = "snake_case" -+ -+# Regular expression matching correct method names. Overrides method-naming- -+# style. If left empty, method names will be checked with the set naming style. -+method-rgx = "(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$" -+ -+# Naming style matching correct module names. -+module-naming-style = "snake_case" -+ -+# Regular expression matching correct module names. Overrides module-naming- -+# style. If left empty, module names will be checked with the set naming style. -+module-rgx = "(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$" -+ -+# Colon-delimited sets of names that determine each other's naming style when the -+# name regexes allow several styles. -+# name-group = -+ -+# Regular expression which should only match function or class names that do not -+# require a docstring. -+no-docstring-rgx = "^_" -+ -+# List of decorators that produce properties, such as abc.abstractproperty. Add -+# to this list to register other decorators that produce valid properties. These -+# decorators are taken in consideration only for invalid-name. -+property-classes = ["abc.abstractproperty"] -+ -+# Regular expression matching correct type variable names. If left empty, type -+# variable names will be checked with the set naming style. -+# typevar-rgx = -+ -+# Naming style matching correct variable names. -+variable-naming-style = "snake_case" -+ -+# Regular expression matching correct variable names. Overrides variable-naming- -+# style. If left empty, variable names will be checked with the set naming style. -+variable-rgx = "(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$" -+ -+[tool.pylint.classes] -+# Warn about protected attribute access inside special methods -+# check-protected-access-in-special-methods = -+ -+# List of method names used to declare (i.e. assign) instance attributes. -+defining-attr-methods = ["__init__", "__new__", "setUp"] -+ -+# List of member names, which should be excluded from the protected access -+# warning. -+exclude-protected = ["_asdict", "_fields", "_replace", "_source", "_make"] -+ -+# List of valid names for the first argument in a class method. -+valid-classmethod-first-arg = ["cls"] -+ -+# List of valid names for the first argument in a metaclass class method. -+valid-metaclass-classmethod-first-arg = ["mcs"] -+ -+[tool.pylint.design] -+# List of regular expressions of class ancestor names to ignore when counting -+# public methods (see R0903) -+# exclude-too-few-public-methods = -+ -+# List of qualified class names to ignore when counting class parents (see R0901) -+# ignored-parents = -+ -+# Maximum number of arguments for function / method. -+max-args = 8 -+ -+# Maximum number of attributes for a class (see R0902). -+max-attributes = 11 -+ -+# Maximum number of boolean expressions in an if statement (see R0916). -+max-bool-expr = 5 -+ -+# Maximum number of branch for function / method body. -+max-branches = 12 -+ -+# Maximum number of locals for function / method body. -+max-locals = 18 -+ -+# Maximum number of parents for a class (see R0901). -+max-parents = 7 -+ -+# Maximum number of public methods for a class (see R0904). -+max-public-methods = 20 -+ -+# Maximum number of return / yield for function / method body. -+max-returns = 6 -+ -+# Maximum number of statements in function / method body. -+max-statements = 50 -+ -+# Minimum number of public methods for a class (see R0903). -+min-public-methods = 1 -+ -+[tool.pylint.exceptions] -+# Exceptions that will emit a warning when caught. -+overgeneral-exceptions = ["Exception"] -+ -+[tool.pylint.format] -+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -+expected-line-ending-format = "LF" -+ -+# Regexp for a line that is allowed to be longer than the limit. -+ignore-long-lines = "^\\s*(# )??$" -+ -+# Number of spaces of indent required inside a hanging or continued line. -+indent-after-paren = 4 -+ -+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -+# tab). -+indent-string = " " -+ -+# Maximum number of characters on a single line. -+max-line-length = 88 -+ -+# Maximum number of lines in a module. -+max-module-lines = 1000 -+ -+# Allow the body of a class to be on the same line as the declaration if body -+# contains single statement. -+# single-line-class-stmt = -+ -+# Allow the body of an if to be on the same line as the test if there is no else. -+# single-line-if-stmt = -+ -+[tool.pylint.imports] -+# List of modules that can be imported at any level, not just the top level one. -+# allow-any-import-level = -+ -+# Allow wildcard imports from modules that define __all__. -+# allow-wildcard-with-all = -+ -+# Deprecated modules which should not be used, separated by a comma. -+deprecated-modules = ["optparse", "tkinter.tix"] -+ -+# Output a graph (.gv or any supported image format) of external dependencies to -+# the given file (report RP0402 must not be disabled). -+# ext-import-graph = -+ -+# Output a graph (.gv or any supported image format) of all (i.e. internal and -+# external) dependencies to the given file (report RP0402 must not be disabled). -+# import-graph = -+ -+# Output a graph (.gv or any supported image format) of internal dependencies to -+# the given file (report RP0402 must not be disabled). -+# int-import-graph = -+ -+# Force import order to recognize a module as part of the standard compatibility -+# libraries. -+# known-standard-library = -+ -+# Force import order to recognize a module as part of a third party library. -+known-third-party = ["enchant"] -+ -+# Couples of modules and preferred modules, separated by a comma. -+# preferred-modules = -+ -+[tool.pylint.logging] -+# The type of string formatting that logging methods do. `old` means using % -+# formatting, `new` is for `{}` formatting. -+logging-format-style = "old" -+ -+# Logging modules to check that the string format arguments are in logging -+# function parameter format. -+logging-modules = ["logging"] -+ -+[tool.pylint."messages control"] -+# Only show warnings with the listed confidence levels. Leave empty to show all. -+# Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -+confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"] -+ -+# Disable the message, report, category or checker with the given id(s). You can -+# either give multiple identifiers separated by comma (,) or put this option -+# multiple times (only on the command line, not in the configuration file where -+# it should appear only once). You can also use "--disable=all" to disable -+# everything first and then re-enable specific checks. For example, if you want -+# to run only the similarities checker, you can use "--disable=all -+# --enable=similarities". If you want to run only the classes checker, but have -+# no Warning level messages displayed, use "--disable=all --enable=classes -+# --disable=W". -+disable = ["raw-checker-failed", "bad-inline-option", "locally-disabled", "file-ignored", "suppressed-message", "useless-suppression", "deprecated-pragma", "use-symbolic-message-instead", "invalid-sequence-index", "anomalous-backslash-in-string", "too-few-public-methods", "consider-using-f-string", "subprocess-run-check"] -+ -+# Enable the message, report, category or checker with the given id(s). You can -+# either give multiple identifier separated by comma (,) or put this option -+# multiple time (only on the command line, not in the configuration file where it -+# should appear only once). See also the "--disable" option for examples. -+enable = ["c-extension-no-member"] -+ -+[tool.pylint.miscellaneous] -+# List of note tags to take in consideration, separated by a comma. -+notes = ["FIXME", "XXX"] -+ -+# Regular expression of note tags to take in consideration. -+# notes-rgx = -+ -+[tool.pylint.refactoring] -+# Maximum number of nested blocks for function / method body -+max-nested-blocks = 5 -+ -+# Complete name of functions that never returns. When checking for inconsistent- -+# return-statements if a never returning function is called then it will be -+# considered as an explicit return statement and no message will be printed. -+never-returning-functions = ["sys.exit", "argparse.parse_error"] -+ -+[tool.pylint.reports] -+# Python expression which should return a score less than or equal to 10. You -+# have access to the variables 'fatal', 'error', 'warning', 'refactor', -+# 'convention', and 'info' which contain the number of messages in each category, -+# as well as 'statement' which is the total number of statements analyzed. This -+# score is used by the global evaluation report (RP0004). -+evaluation = "10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)" -+ -+# Template used to display messages. This is a python new-style format string -+# used to format the message information. See doc for all details. -+# msg-template = -+ -+# Set the output format. Available formats are text, parseable, colorized, json -+# and msvs (visual studio). You can also give a reporter class, e.g. -+# mypackage.mymodule.MyReporterClass. -+# output-format = -+ -+# Tells whether to display a full report or only the messages. -+# reports = -+ -+# Activate the evaluation score. -+score = true -+ -+[tool.pylint.similarities] -+# Comments are removed from the similarity computation -+ignore-comments = true -+ -+# Docstrings are removed from the similarity computation -+ignore-docstrings = true -+ -+# Imports are removed from the similarity computation -+# ignore-imports = -+ -+# Signatures are removed from the similarity computation -+ignore-signatures = true -+ -+# Minimum lines number of a similarity. -+min-similarity-lines = 4 -+ -+[tool.pylint.spelling] -+# Limits count of emitted suggestions for spelling mistakes. -+max-spelling-suggestions = 4 -+ -+# Spelling dictionary name. Available dictionaries: none. To make it work, -+# install the 'python-enchant' package. -+# spelling-dict = -+ -+# List of comma separated words that should be considered directives if they -+# appear at the beginning of a comment and should not be checked. -+spelling-ignore-comment-directives = "fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:" -+ -+# List of comma separated words that should not be checked. -+# spelling-ignore-words = -+ -+# A path to a file that contains the private dictionary; one word per line. -+# spelling-private-dict-file = -+ -+# Tells whether to store unknown words to the private dictionary (see the -+# --spelling-private-dict-file option) instead of raising a message. -+# spelling-store-unknown-words = -+ -+[tool.pylint.string] -+# This flag controls whether inconsistent-quotes generates a warning when the -+# character used as a quote delimiter is used inconsistently within a module. -+# check-quote-consistency = -+ -+# This flag controls whether the implicit-str-concat should generate a warning on -+# implicit string concatenation in sequences defined over several lines. -+# check-str-concat-over-line-jumps = -+ -+[tool.pylint.typecheck] -+# List of decorators that produce context managers, such as -+# contextlib.contextmanager. Add to this list to register other decorators that -+# produce valid context managers. -+contextmanager-decorators = ["contextlib.contextmanager"] -+ -+# List of members which are set dynamically and missed by pylint inference -+# system, and so shouldn't trigger E1101 when accessed. Python regular -+# expressions are accepted. -+# generated-members = -+ -+# Tells whether missing members accessed in mixin class should be ignored. A -+# class is considered mixin if its name matches the mixin-class-rgx option. -+# Tells whether to warn about missing members when the owner of the attribute is -+# inferred to be None. -+ignore-none = true -+ -+# This flag controls whether pylint should warn about no-member and similar -+# checks whenever an opaque object is returned when inferring. The inference can -+# return multiple potential results while evaluating a Python object, but some -+# branches might not be evaluated, which results in partial inference. In that -+# case, it might be useful to still emit no-member and other checks for the rest -+# of the inferred objects. -+ignore-on-opaque-inference = true -+ -+# List of symbolic message names to ignore for Mixin members. -+ignored-checks-for-mixins = ["no-member", "not-async-context-manager", "not-context-manager", "attribute-defined-outside-init"] -+ -+# List of class names for which member attributes should not be checked (useful -+# for classes with dynamically set attributes). This supports the use of -+# qualified names. -+ignored-classes = ["optparse.Values", "thread._local", "_thread._local"] -+ -+# Show a hint with possible names when a member name was not found. The aspect of -+# finding the hint is based on edit distance. -+missing-member-hint = true -+ -+# The minimum edit distance a name should have in order to be considered a -+# similar match for a missing member name. -+missing-member-hint-distance = 1 -+ -+# The total number of similar names that should be taken in consideration when -+# showing a hint for a missing member. -+missing-member-max-choices = 1 -+ -+# Regex pattern to define which classes are considered mixins. -+mixin-class-rgx = ".*[Mm]ixin" -+ -+# List of decorators that change the signature of a decorated function. -+# signature-mutators = -+ -+[tool.pylint.variables] -+# List of additional names supposed to be defined in builtins. Remember that you -+# should avoid defining new builtins when possible. -+# additional-builtins = -+ -+# Tells whether unused global variables should be treated as a violation. -+allow-global-unused-variables = true -+ -+# List of names allowed to shadow builtins -+# allowed-redefined-builtins = -+ -+# List of strings which can identify a callback function by name. A callback name -+# must start or end with one of those strings. -+callbacks = ["cb_", "_cb", "_callback"] -+ -+# A regular expression matching the name of dummy variables (i.e. expected to not -+# be used). -+dummy-variables-rgx = "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_" -+ -+# Argument names that match this expression will be ignored. Default to name with -+# leading underscore. -+ignored-argument-names = "_.*|^ignored_|^unused_" -+ -+# Tells whether we should check for unused import in __init__ files. -+# init-import = -+ -+# List of qualified module names which can have objects that can redefine -+# builtins. -+redefining-builtins-modules = ["six.moves", "future.builtins"] -diff --git a/requirements-dev.txt b/requirements-dev.txt -new file mode 100644 -index 0000000..fe21d76 ---- /dev/null -+++ b/requirements-dev.txt -@@ -0,0 +1,7 @@ -+coverage[toml] -+mypy -+pylint -+pytest -+rich -+types-PyYAML -+types-requests -diff --git a/requirements.txt b/requirements.txt -new file mode 100644 -index 0000000..1c6d8b4 ---- /dev/null -+++ b/requirements.txt -@@ -0,0 +1,2 @@ -+pyyaml -+requests -diff --git a/setup.py b/setup.py -new file mode 100644 -index 0000000..1698e52 ---- /dev/null -+++ b/setup.py -@@ -0,0 +1,11 @@ -+#!/usr/bin/env python -+"""Bootstrapper for docker's ENTRYPOINT executable. -+ -+Since using setup.py is no longer std convention, -+all install information is located in pyproject.toml -+""" -+ -+import setuptools -+ -+ -+setuptools.setup() -diff --git a/tests/capture_tools_output/.clang-format b/tests/capture_tools_output/.clang-format -new file mode 100644 -index 0000000..6997192 ---- /dev/null -+++ b/tests/capture_tools_output/.clang-format -@@ -0,0 +1,148 @@ -+--- -+Language: Cpp -+# BasedOnStyle: LLVM -+AccessModifierOffset: -2 -+AlignAfterOpenBracket: Align -+AlignConsecutiveMacros: true -+AlignConsecutiveAssignments: false -+AlignConsecutiveBitFields: false -+AlignConsecutiveDeclarations: false -+AlignEscapedNewlines: Right -+AlignOperands: Align -+AlignTrailingComments: true -+AllowAllArgumentsOnNextLine: true -+AllowAllConstructorInitializersOnNextLine: true -+AllowAllParametersOfDeclarationOnNextLine: true -+AllowShortEnumsOnASingleLine: true -+AllowShortBlocksOnASingleLine: Never -+AllowShortCaseLabelsOnASingleLine: false -+AllowShortFunctionsOnASingleLine: None -+AllowShortLambdasOnASingleLine: All -+AllowShortIfStatementsOnASingleLine: Never -+AllowShortLoopsOnASingleLine: false -+AlwaysBreakAfterDefinitionReturnType: None -+AlwaysBreakAfterReturnType: None -+AlwaysBreakBeforeMultilineStrings: false -+AlwaysBreakTemplateDeclarations: MultiLine -+BinPackArguments: true -+BinPackParameters: true -+BraceWrapping: -+ AfterCaseLabel: false -+ AfterClass: true -+ AfterControlStatement: Always -+ AfterEnum: true -+ AfterFunction: true -+ AfterNamespace: true -+ AfterObjCDeclaration: false -+ AfterStruct: true -+ AfterUnion: true -+ AfterExternBlock: false -+ BeforeCatch: false -+ BeforeElse: true -+ BeforeLambdaBody: false -+ BeforeWhile: false -+ IndentBraces: false -+ SplitEmptyFunction: true -+ SplitEmptyRecord: true -+ SplitEmptyNamespace: true -+BreakBeforeBinaryOperators: None -+BreakBeforeBraces: Custom -+BreakBeforeInheritanceComma: false -+BreakInheritanceList: BeforeColon -+BreakBeforeTernaryOperators: true -+BreakConstructorInitializersBeforeComma: false -+BreakConstructorInitializers: BeforeColon -+BreakAfterJavaFieldAnnotations: false -+BreakStringLiterals: true -+ColumnLimit: 80 -+CommentPragmas: '^ IWYU pragma:' -+CompactNamespaces: false -+ConstructorInitializerAllOnOneLineOrOnePerLine: false -+ConstructorInitializerIndentWidth: 4 -+ContinuationIndentWidth: 4 -+Cpp11BracedListStyle: true -+DeriveLineEnding: true -+DerivePointerAlignment: false -+DisableFormat: false -+ExperimentalAutoDetectBinPacking: false -+FixNamespaceComments: true -+ForEachMacros: -+ - foreach -+ - Q_FOREACH -+ - BOOST_FOREACH -+IncludeBlocks: Preserve -+IncludeCategories: -+ - Regex: '^"(llvm|llvm-c|clang|clang-c)/' -+ Priority: 2 -+ SortPriority: 0 -+ - Regex: '^(<|"(gtest|gmock|isl|json)/)' -+ Priority: 3 -+ SortPriority: 0 -+ - Regex: '.*' -+ Priority: 1 -+ SortPriority: 0 -+IncludeIsMainRegex: '(Test)?$' -+IncludeIsMainSourceRegex: '' -+IndentCaseLabels: false -+IndentCaseBlocks: false -+IndentGotoLabels: true -+IndentPPDirectives: None -+IndentExternBlock: AfterExternBlock -+IndentWidth: 4 -+IndentWrappedFunctionNames: false -+InsertTrailingCommas: None -+JavaScriptQuotes: Leave -+JavaScriptWrapImports: true -+KeepEmptyLinesAtTheStartOfBlocks: true -+MacroBlockBegin: '' -+MacroBlockEnd: '' -+MaxEmptyLinesToKeep: 2 -+NamespaceIndentation: None -+ObjCBinPackProtocolList: Auto -+ObjCBlockIndentWidth: 2 -+ObjCBreakBeforeNestedBlockParam: true -+ObjCSpaceAfterProperty: false -+ObjCSpaceBeforeProtocolList: true -+PenaltyBreakAssignment: 2 -+PenaltyBreakBeforeFirstCallParameter: 19 -+PenaltyBreakComment: 300 -+PenaltyBreakFirstLessLess: 120 -+PenaltyBreakString: 1000 -+PenaltyBreakTemplateDeclaration: 10 -+PenaltyExcessCharacter: 1000000 -+PenaltyReturnTypeOnItsOwnLine: 60 -+PointerAlignment: Right -+ReflowComments: false -+SortIncludes: false -+SortUsingDeclarations: true -+SpaceAfterCStyleCast: false -+SpaceAfterLogicalNot: false -+SpaceAfterTemplateKeyword: true -+SpaceBeforeAssignmentOperators: true -+SpaceBeforeCpp11BracedList: false -+SpaceBeforeCtorInitializerColon: true -+SpaceBeforeInheritanceColon: true -+SpaceBeforeParens: ControlStatements -+SpaceBeforeRangeBasedForLoopColon: true -+SpaceInEmptyBlock: false -+SpaceInEmptyParentheses: false -+SpacesBeforeTrailingComments: 1 -+SpacesInAngles: false -+SpacesInConditionalStatement: false -+SpacesInContainerLiterals: true -+SpacesInCStyleCastParentheses: false -+SpacesInParentheses: false -+SpacesInSquareBrackets: false -+SpaceBeforeSquareBrackets: false -+Standard: Latest -+StatementMacros: -+ - Q_UNUSED -+ - QT_REQUIRE_VERSION -+TabWidth: 8 -+UseCRLF: false -+UseTab: Never -+WhitespaceSensitiveMacros: -+ - STRINGIZE -+ - PP_STRINGIZE -+ - BOOST_PP_STRINGIZE -+... -diff --git a/tests/capture_tools_output/.clang-tidy b/tests/capture_tools_output/.clang-tidy -new file mode 100644 -index 0000000..19dc3ea ---- /dev/null -+++ b/tests/capture_tools_output/.clang-tidy -@@ -0,0 +1,349 @@ -+--- -+Checks: 'clang-diagnostic-*,clang-analyzer-*,-boost-*,bugprone-*,performance-*,readability-*,portability-*,modernize-*,clang-analyzer-*,cppcoreguidelines-*,-cppcoreguidelines-avoid-magic-numbers,-readability-magic-numbers' -+WarningsAsErrors: '' -+HeaderFilterRegex: '' -+AnalyzeTemporaryDtors: false -+FormatStyle: none -+User: ytreh -+CheckOptions: -+ - key: modernize-replace-auto-ptr.IncludeStyle -+ value: llvm -+ - key: cppcoreguidelines-no-malloc.Reallocations -+ value: '::realloc' -+ - key: cppcoreguidelines-owning-memory.LegacyResourceConsumers -+ value: '::free;::realloc;::freopen;::fclose' -+ - key: readability-static-accessed-through-instance.NameSpecifierNestingThreshold -+ value: '3' -+ - key: readability-function-size.VariableThreshold -+ value: '4294967295' -+ - key: modernize-use-auto.MinTypeNameLength -+ value: '5' -+ - key: bugprone-reserved-identifier.Invert -+ value: 'false' -+ - key: performance-move-const-arg.CheckTriviallyCopyableMove -+ value: 'true' -+ - key: cert-dcl16-c.NewSuffixes -+ value: 'L;LL;LU;LLU' -+ - key: bugprone-narrowing-conversions.WarnOnFloatingPointNarrowingConversion -+ value: 'true' -+ - key: readability-identifier-naming.GetConfigPerFile -+ value: 'true' -+ - key: bugprone-narrowing-conversions.PedanticMode -+ value: 'false' -+ - key: readability-inconsistent-declaration-parameter-name.Strict -+ value: 'false' -+ - key: cppcoreguidelines-macro-usage.CheckCapsOnly -+ value: 'false' -+ - key: bugprone-unused-return-value.CheckedFunctions -+ value: '::std::async;::std::launder;::std::remove;::std::remove_if;::std::unique;::std::unique_ptr::release;::std::basic_string::empty;::std::vector::empty;::std::back_inserter;::std::distance;::std::find;::std::find_if;::std::inserter;::std::lower_bound;::std::make_pair;::std::map::count;::std::map::find;::std::map::lower_bound;::std::multimap::equal_range;::std::multimap::upper_bound;::std::set::count;::std::set::find;::std::setfill;::std::setprecision;::std::setw;::std::upper_bound;::std::vector::at;::bsearch;::ferror;::feof;::isalnum;::isalpha;::isblank;::iscntrl;::isdigit;::isgraph;::islower;::isprint;::ispunct;::isspace;::isupper;::iswalnum;::iswprint;::iswspace;::isxdigit;::memchr;::memcmp;::strcmp;::strcoll;::strncmp;::strpbrk;::strrchr;::strspn;::strstr;::wcscmp;::access;::bind;::connect;::difftime;::dlsym;::fnmatch;::getaddrinfo;::getopt;::htonl;::htons;::iconv_open;::inet_addr;::isascii;::isatty;::mmap;::newlocale;::openat;::pathconf;::pthread_equal;::pthread_getspecific;::pthread_mutex_trylock;::readdir;::readlink;::recvmsg;::regexec;::scandir;::semget;::setjmp;::shm_open;::shmget;::sigismember;::strcasecmp;::strsignal;::ttyname' -+ - key: modernize-use-default-member-init.UseAssignment -+ value: 'false' -+ - key: readability-function-size.NestingThreshold -+ value: '4294967295' -+ - key: modernize-use-override.AllowOverrideAndFinal -+ value: 'false' -+ - key: readability-function-size.ParameterThreshold -+ value: '4294967295' -+ - key: modernize-pass-by-value.ValuesOnly -+ value: 'false' -+ - key: modernize-loop-convert.IncludeStyle -+ value: llvm -+ - key: cert-str34-c.DiagnoseSignedUnsignedCharComparisons -+ value: '0' -+ - key: bugprone-suspicious-string-compare.WarnOnLogicalNotComparison -+ value: 'false' -+ - key: cppcoreguidelines-explicit-virtual-functions.AllowOverrideAndFinal -+ value: 'false' -+ - key: readability-redundant-smartptr-get.IgnoreMacros -+ value: 'true' -+ - key: readability-identifier-naming.AggressiveDependentMemberLookup -+ value: 'false' -+ - key: bugprone-suspicious-string-compare.WarnOnImplicitComparison -+ value: 'true' -+ - key: modernize-use-emplace.TupleTypes -+ value: '::std::pair;::std::tuple' -+ - key: modernize-use-emplace.TupleMakeFunctions -+ value: '::std::make_pair;::std::make_tuple' -+ - key: cppcoreguidelines-owning-memory.LegacyResourceProducers -+ value: '::malloc;::aligned_alloc;::realloc;::calloc;::fopen;::freopen;::tmpfile' -+ - key: bugprone-argument-comment.CommentNullPtrs -+ value: '0' -+ - key: bugprone-argument-comment.StrictMode -+ value: '0' -+ - key: cppcoreguidelines-init-variables.IncludeStyle -+ value: llvm -+ - key: modernize-use-nodiscard.ReplacementString -+ value: '[[nodiscard]]' -+ - key: modernize-loop-convert.MakeReverseRangeHeader -+ value: '' -+ - key: modernize-replace-random-shuffle.IncludeStyle -+ value: llvm -+ - key: cppcoreguidelines-narrowing-conversions.WarnOnFloatingPointNarrowingConversion -+ value: 'true' -+ - key: modernize-use-bool-literals.IgnoreMacros -+ value: 'true' -+ - key: bugprone-unhandled-self-assignment.WarnOnlyIfThisHasSuspiciousField -+ value: 'true' -+ - key: google-readability-namespace-comments.ShortNamespaceLines -+ value: '10' -+ - key: bugprone-suspicious-string-compare.StringCompareLikeFunctions -+ value: '' -+ - key: modernize-avoid-bind.PermissiveParameterList -+ value: 'false' -+ - key: modernize-use-override.FinalSpelling -+ value: final -+ - key: performance-move-constructor-init.IncludeStyle -+ value: llvm -+ - key: modernize-loop-convert.UseCxx20ReverseRanges -+ value: 'true' -+ - key: modernize-use-noexcept.ReplacementString -+ value: '' -+ - key: modernize-use-using.IgnoreMacros -+ value: 'true' -+ - key: performance-type-promotion-in-math-fn.IncludeStyle -+ value: llvm -+ - key: cppcoreguidelines-explicit-virtual-functions.FinalSpelling -+ value: final -+ - key: modernize-loop-convert.NamingStyle -+ value: CamelCase -+ - key: bugprone-suspicious-include.ImplementationFileExtensions -+ value: 'c;cc;cpp;cxx' -+ - key: cppcoreguidelines-pro-type-member-init.UseAssignment -+ value: 'false' -+ - key: modernize-loop-convert.MakeReverseRangeFunction -+ value: '' -+ - key: bugprone-suspicious-include.HeaderFileExtensions -+ value: ';h;hh;hpp;hxx' -+ - key: performance-no-automatic-move.AllowedTypes -+ value: '' -+ - key: performance-for-range-copy.WarnOnAllAutoCopies -+ value: 'false' -+ - key: bugprone-argument-comment.CommentIntegerLiterals -+ value: '0' -+ - key: bugprone-suspicious-missing-comma.SizeThreshold -+ value: '5' -+ - key: readability-inconsistent-declaration-parameter-name.IgnoreMacros -+ value: 'true' -+ - key: readability-identifier-naming.IgnoreFailedSplit -+ value: 'false' -+ - key: modernize-pass-by-value.IncludeStyle -+ value: llvm -+ - key: bugprone-sizeof-expression.WarnOnSizeOfThis -+ value: 'true' -+ - key: readability-qualified-auto.AddConstToQualified -+ value: 'true' -+ - key: bugprone-string-constructor.WarnOnLargeLength -+ value: 'true' -+ - key: bugprone-too-small-loop-variable.MagnitudeBitsUpperLimit -+ value: '16' -+ - key: readability-simplify-boolean-expr.ChainedConditionalReturn -+ value: 'false' -+ - key: cppcoreguidelines-explicit-virtual-functions.OverrideSpelling -+ value: override -+ - key: readability-else-after-return.WarnOnConditionVariables -+ value: 'true' -+ - key: readability-uppercase-literal-suffix.IgnoreMacros -+ value: 'true' -+ - key: modernize-use-nullptr.NullMacros -+ value: 'NULL' -+ - key: modernize-make-shared.IgnoreMacros -+ value: 'true' -+ - key: bugprone-dynamic-static-initializers.HeaderFileExtensions -+ value: ';h;hh;hpp;hxx' -+ - key: bugprone-suspicious-enum-usage.StrictMode -+ value: 'false' -+ - key: performance-unnecessary-copy-initialization.AllowedTypes -+ value: '' -+ - key: bugprone-suspicious-missing-comma.MaxConcatenatedTokens -+ value: '5' -+ - key: modernize-use-transparent-functors.SafeMode -+ value: 'false' -+ - key: cppcoreguidelines-macro-usage.AllowedRegexp -+ value: '^DEBUG_*' -+ - key: modernize-make-shared.IgnoreDefaultInitialization -+ value: 'true' -+ - key: bugprone-argument-comment.CommentCharacterLiterals -+ value: '0' -+ - key: cppcoreguidelines-narrowing-conversions.PedanticMode -+ value: 'false' -+ - key: bugprone-not-null-terminated-result.WantToUseSafeFunctions -+ value: 'true' -+ - key: modernize-make-shared.IncludeStyle -+ value: llvm -+ - key: bugprone-string-constructor.LargeLengthThreshold -+ value: '8388608' -+ - key: readability-simplify-boolean-expr.ChainedConditionalAssignment -+ value: 'false' -+ - key: cppcoreguidelines-special-member-functions.AllowMissingMoveFunctions -+ value: 'false' -+ - key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField -+ value: '0' -+ - key: bugprone-exception-escape.FunctionsThatShouldNotThrow -+ value: '' -+ - key: bugprone-signed-char-misuse.CharTypdefsToIgnore -+ value: '' -+ - key: performance-inefficient-vector-operation.EnableProto -+ value: 'false' -+ - key: modernize-loop-convert.MaxCopySize -+ value: '16' -+ - key: bugprone-argument-comment.CommentFloatLiterals -+ value: '0' -+ - key: readability-function-size.LineThreshold -+ value: '4294967295' -+ - key: portability-simd-intrinsics.Suggest -+ value: 'false' -+ - key: modernize-make-shared.MakeSmartPtrFunction -+ value: 'std::make_shared' -+ - key: cppcoreguidelines-explicit-virtual-functions.IgnoreDestructors -+ value: 'true' -+ - key: cppcoreguidelines-pro-bounds-constant-array-index.GslHeader -+ value: '' -+ - key: modernize-make-unique.IgnoreMacros -+ value: 'true' -+ - key: modernize-make-shared.MakeSmartPtrFunctionHeader -+ value: '' -+ - key: performance-for-range-copy.AllowedTypes -+ value: '' -+ - key: modernize-use-override.IgnoreDestructors -+ value: 'false' -+ - key: bugprone-sizeof-expression.WarnOnSizeOfConstant -+ value: 'true' -+ - key: readability-redundant-string-init.StringNames -+ value: '::std::basic_string_view;::std::basic_string' -+ - key: modernize-make-unique.IgnoreDefaultInitialization -+ value: 'true' -+ - key: modernize-use-emplace.ContainersWithPushBack -+ value: '::std::vector;::std::list;::std::deque' -+ - key: modernize-make-unique.IncludeStyle -+ value: llvm -+ - key: readability-braces-around-statements.ShortStatementLines -+ value: '0' -+ - key: bugprone-argument-comment.CommentUserDefinedLiterals -+ value: '0' -+ - key: bugprone-argument-comment.CommentBoolLiterals -+ value: '0' -+ - key: modernize-use-override.OverrideSpelling -+ value: override -+ - key: performance-inefficient-string-concatenation.StrictMode -+ value: 'false' -+ - key: readability-implicit-bool-conversion.AllowPointerConditions -+ value: 'false' -+ - key: readability-redundant-declaration.IgnoreMacros -+ value: 'true' -+ - key: google-readability-braces-around-statements.ShortStatementLines -+ value: '1' -+ - key: modernize-make-unique.MakeSmartPtrFunction -+ value: 'std::make_unique' -+ - key: cppcoreguidelines-pro-type-member-init.IgnoreArrays -+ value: 'false' -+ - key: readability-else-after-return.WarnOnUnfixable -+ value: 'true' -+ - key: bugprone-reserved-identifier.AllowedIdentifiers -+ value: '' -+ - key: modernize-use-emplace.IgnoreImplicitConstructors -+ value: 'false' -+ - key: modernize-make-unique.MakeSmartPtrFunctionHeader -+ value: '' -+ - key: portability-restrict-system-includes.Includes -+ value: '*' -+ - key: modernize-use-equals-delete.IgnoreMacros -+ value: 'true' -+ - key: cppcoreguidelines-pro-bounds-constant-array-index.IncludeStyle -+ value: llvm -+ - key: cppcoreguidelines-macro-usage.IgnoreCommandLineMacros -+ value: 'true' -+ - key: bugprone-misplaced-widening-cast.CheckImplicitCasts -+ value: 'false' -+ - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnorePublicMemberVariables -+ value: 'false' -+ - key: modernize-loop-convert.MinConfidence -+ value: reasonable -+ - key: performance-unnecessary-value-param.AllowedTypes -+ value: '' -+ - key: bugprone-suspicious-missing-comma.RatioThreshold -+ value: '0.200000' -+ - key: cppcoreguidelines-special-member-functions.AllowMissingMoveFunctionsWhenCopyIsDeleted -+ value: 'false' -+ - key: readability-uppercase-literal-suffix.NewSuffixes -+ value: '' -+ - key: google-readability-namespace-comments.SpacesBeforeComments -+ value: '2' -+ - key: readability-function-cognitive-complexity.Threshold -+ value: '25' -+ - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic -+ value: 'true' -+ - key: bugprone-argument-comment.IgnoreSingleArgument -+ value: '0' -+ - key: cppcoreguidelines-no-malloc.Allocations -+ value: '::malloc;::calloc' -+ - key: modernize-use-noexcept.UseNoexceptFalse -+ value: 'true' -+ - key: bugprone-sizeof-expression.WarnOnSizeOfIntegerExpression -+ value: 'false' -+ - key: performance-faster-string-find.StringLikeClasses -+ value: '::std::basic_string;::std::basic_string_view' -+ - key: bugprone-assert-side-effect.CheckFunctionCalls -+ value: 'false' -+ - key: readability-function-size.BranchThreshold -+ value: '4294967295' -+ - key: bugprone-string-constructor.StringNames -+ value: '::std::basic_string;::std::basic_string_view' -+ - key: bugprone-assert-side-effect.AssertMacros -+ value: assert -+ - key: bugprone-exception-escape.IgnoredExceptions -+ value: '' -+ - key: readability-function-size.StatementThreshold -+ value: '800' -+ - key: modernize-use-default-member-init.IgnoreMacros -+ value: 'true' -+ - key: llvm-qualified-auto.AddConstToQualified -+ value: '0' -+ - key: bugprone-argument-comment.CommentStringLiterals -+ value: '0' -+ - key: readability-identifier-naming.IgnoreMainLikeFunctions -+ value: 'false' -+ - key: bugprone-signed-char-misuse.DiagnoseSignedUnsignedCharComparisons -+ value: 'true' -+ - key: readability-implicit-bool-conversion.AllowIntegerConditions -+ value: 'false' -+ - key: cppcoreguidelines-init-variables.MathHeader -+ value: '' -+ - key: google-readability-function-size.StatementThreshold -+ value: '800' -+ - key: llvm-else-after-return.WarnOnConditionVariables -+ value: '0' -+ - key: bugprone-sizeof-expression.WarnOnSizeOfCompareToConstant -+ value: 'true' -+ - key: bugprone-reserved-identifier.AggressiveDependentMemberLookup -+ value: 'false' -+ - key: modernize-raw-string-literal.DelimiterStem -+ value: lit -+ - key: modernize-use-equals-default.IgnoreMacros -+ value: 'true' -+ - key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor -+ value: 'false' -+ - key: modernize-raw-string-literal.ReplaceShorterLiterals -+ value: 'false' -+ - key: modernize-use-emplace.SmartPointers -+ value: '::std::shared_ptr;::std::unique_ptr;::std::auto_ptr;::std::weak_ptr' -+ - key: cppcoreguidelines-no-malloc.Deallocations -+ value: '::free' -+ - key: modernize-use-auto.RemoveStars -+ value: 'false' -+ - key: bugprone-dangling-handle.HandleClasses -+ value: 'std::basic_string_view;std::experimental::basic_string_view' -+ - key: performance-inefficient-vector-operation.VectorLikeClasses -+ value: '::std::vector' -+ - key: portability-simd-intrinsics.Std -+ value: '' -+ - key: performance-unnecessary-value-param.IncludeStyle -+ value: llvm -+ - key: readability-redundant-member-init.IgnoreBaseInCopyConstructors -+ value: 'false' -+ - key: modernize-replace-disallow-copy-and-assign-macro.MacroName -+ value: DISALLOW_COPY_AND_ASSIGN -+ - key: llvm-else-after-return.WarnOnUnfixable -+ value: '0' -+ - key: readability-simplify-subscript-expr.Types -+ value: '::std::basic_string;::std::basic_string_view;::std::vector;::std::array' -+... -diff --git a/tests/capture_tools_output/event_files.json b/tests/capture_tools_output/event_files.json -new file mode 100644 -index 0000000..ba8947a ---- /dev/null -+++ b/tests/capture_tools_output/event_files.json -@@ -0,0 +1,230 @@ -+[ -+ { -+ "sha": "7f1eed09c6c07682738de7b5141fd151d16cf368", -+ "filename": "CMakeLists.txt", -+ "status": "modified", -+ "additions": 14, -+ "deletions": 2, -+ "changes": 16, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/CMakeLists.txt", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/CMakeLists.txt", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/CMakeLists.txt?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -28,9 +28,21 @@ else()\n \"-Wredundant-decls\")\n endif()\n \n+option(ENABLE_SDL2_NET \"Enable SDL2_net\" On)\n+option(ENABLE_SDL2_MIXER \"Enable SDL2_mixer\" On)\n+\n find_package(SDL2 2.0.7)\n-find_package(SDL2_mixer 2.0.2)\n-find_package(SDL2_net 2.0.0)\n+if(ENABLE_SDL2_MIXER)\n+ find_package(SDL2_mixer 2.0.2)\n+else()\n+ add_compile_definitions(DISABLE_SDL2MIXER=1)\n+endif()\n+\n+if(ENABLE_SDL2_NET)\n+ find_package(SDL2_net 2.0.0)\n+else()\n+ add_compile_definitions(DISABLE_SDL2NET=1)\n+endif()\n \n # Check for libsamplerate.\n find_package(samplerate)" -+ }, -+ { -+ "sha": "fd5d8bcecd8a6edb6d77c8686ae32a995b571066", -+ "filename": "configure.ac", -+ "status": "modified", -+ "additions": 17, -+ "deletions": 2, -+ "changes": 19, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/configure.ac", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/configure.ac", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/configure.ac?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -32,8 +32,23 @@ then\n fi\n \n PKG_CHECK_MODULES(SDL, [sdl2 >= 2.0.7])\n-PKG_CHECK_MODULES(SDLMIXER, [SDL2_mixer >= 2.0.2])\n-PKG_CHECK_MODULES(SDLNET, [SDL2_net >= 2.0.0])\n+# Check for SDL2_mixer\n+AC_ARG_ENABLE([sdl2mixer],\n+AS_HELP_STRING([--disable-sdl2mixer], [Disable SDL2_mixer support])\n+)\n+AS_IF([test \"x$enable_sdl2mixer\" != xno], [\n+ PKG_CHECK_MODULES(SDLMIXER, [SDL2_mixer >= 2.0.2])], [\n+ AC_DEFINE([DISABLE_SDL2MIXER], [1], [SDL2_mixer disabled])\n+])\n+\n+# Check for networking\n+AC_ARG_ENABLE([sdl2net],\n+AS_HELP_STRING([--disable-sdl2net], [Disable SDL2_net support])\n+)\n+AS_IF([test \"x$enable_sdl2net\" != xno], [\n+ PKG_CHECK_MODULES(SDLNET, [SDL2_net >= 2.0.0])], [\n+ AC_DEFINE([DISABLE_SDL2NET], [1], [SDL2_net disabled])\n+])\n \n # Check for bash-completion.\n AC_ARG_ENABLE([bash-completion]," -+ }, -+ { -+ "sha": "151f7617dde7d1216f7212b45bb5aeb7661bc86b", -+ "filename": "opl/CMakeLists.txt", -+ "status": "modified", -+ "additions": 4, -+ "deletions": 1, -+ "changes": 5, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/opl%2FCMakeLists.txt", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/opl%2FCMakeLists.txt", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/opl%2FCMakeLists.txt?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -12,4 +12,7 @@ add_library(opl STATIC\n target_include_directories(opl\n INTERFACE \".\"\n PRIVATE \"${CMAKE_CURRENT_BINARY_DIR}/../\")\n-target_link_libraries(opl SDL2::mixer)\n+target_link_libraries(opl SDL2::SDL2)\n+if(ENABLE_SDL2_mixer)\n+ target_link_libraries(opl SDL2::mixer)\n+endif()" -+ }, -+ { -+ "sha": "46e082cf77c5e60affc7689f7357c926fc00cda3", -+ "filename": "opl/opl.c", -+ "status": "modified", -+ "additions": 2, -+ "deletions": 0, -+ "changes": 2, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/opl%2Fopl.c", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/opl%2Fopl.c", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/opl%2Fopl.c?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -50,7 +50,9 @@ static opl_driver_t *drivers[] =\n #ifdef _WIN32\n &opl_win32_driver,\n #endif\n+#ifndef DISABLE_SDL2MIXER\n &opl_sdl_driver,\n+#endif // DISABLE_SDL2MIXER\n NULL\n };\n " -+ }, -+ { -+ "sha": "6bd4e7e1f3374e289d3dc19972dcb1d5379d6b03", -+ "filename": "opl/opl_sdl.c", -+ "status": "modified", -+ "additions": 6, -+ "deletions": 0, -+ "changes": 6, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/opl%2Fopl_sdl.c", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/opl%2Fopl_sdl.c", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/opl%2Fopl_sdl.c?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -33,6 +33,10 @@\n \n #include \"opl_queue.h\"\n \n+\n+#ifndef DISABLE_SDL2MIXER\n+\n+\n #define MAX_SOUND_SLICE_TIME 100 /* ms */\n \n typedef struct\n@@ -511,3 +515,5 @@ opl_driver_t opl_sdl_driver =\n OPL_SDL_AdjustCallbacks,\n };\n \n+\n+#endif // DISABLE_SDL2MIXER" -+ }, -+ { -+ "sha": "9924263aea24261091948e455dfd2521787a04c4", -+ "filename": "pcsound/CMakeLists.txt", -+ "status": "modified", -+ "additions": 4, -+ "deletions": 1, -+ "changes": 5, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/pcsound%2FCMakeLists.txt", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/pcsound%2FCMakeLists.txt", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/pcsound%2FCMakeLists.txt?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -8,4 +8,7 @@ add_library(pcsound STATIC\n target_include_directories(pcsound\n INTERFACE \".\"\n PRIVATE \"${CMAKE_CURRENT_BINARY_DIR}/../\")\n-target_link_libraries(pcsound SDL2::mixer)\n+target_link_libraries(pcsound SDL2::SDL2)\n+if(ENABLE_SDL2_mixer)\n+ target_link_libraries(pcsound SDL2::mixer)\n+endif()" -+ }, -+ { -+ "sha": "58f9b75c2affd5f1dd990aaedab79d834575bf83", -+ "filename": "pcsound/pcsound.c", -+ "status": "modified", -+ "additions": 2, -+ "deletions": 0, -+ "changes": 2, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/pcsound%2Fpcsound.c", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/pcsound%2Fpcsound.c", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/pcsound%2Fpcsound.c?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -56,7 +56,9 @@ static pcsound_driver_t *drivers[] =\n #ifdef _WIN32\n &pcsound_win32_driver,\n #endif\n+#ifndef DISABLE_SDL2MIXER\n &pcsound_sdl_driver,\n+#endif // DISABLE_SDL2MIXER\n NULL,\n };\n " -+ }, -+ { -+ "sha": "e77c7b0d29de1b0c665686004dfda311c9d96719", -+ "filename": "pcsound/pcsound_sdl.c", -+ "status": "modified", -+ "additions": 6, -+ "deletions": 0, -+ "changes": 6, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/pcsound%2Fpcsound_sdl.c", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/pcsound%2Fpcsound_sdl.c", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/pcsound%2Fpcsound_sdl.c?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -24,6 +24,10 @@\n #include \"pcsound.h\"\n #include \"pcsound_internal.h\"\n \n+\n+#ifndef DISABLE_SDL2MIXER\n+\n+\n #define MAX_SOUND_SLICE_TIME 70 /* ms */\n #define SQUARE_WAVE_AMP 0x2000\n \n@@ -248,3 +252,5 @@ pcsound_driver_t pcsound_sdl_driver =\n PCSound_SDL_Shutdown,\n };\n \n+\n+#endif // DISABLE_SDL2MIXER" -+ }, -+ { -+ "sha": "bbb877641f2a8de6e6eb7dad5b8c866e147faf7d", -+ "filename": "src/CMakeLists.txt", -+ "status": "modified", -+ "additions": 18, -+ "deletions": 3, -+ "changes": 21, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2FCMakeLists.txt", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2FCMakeLists.txt", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/src%2FCMakeLists.txt?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -32,7 +32,10 @@ set(DEDSERV_FILES\n add_executable(\"${PROGRAM_PREFIX}server\" WIN32 ${COMMON_SOURCE_FILES} ${DEDSERV_FILES})\n target_include_directories(\"${PROGRAM_PREFIX}server\"\n PRIVATE \"${CMAKE_CURRENT_BINARY_DIR}/../\")\n-target_link_libraries(\"${PROGRAM_PREFIX}server\" SDL2::SDL2main SDL2::net)\n+target_link_libraries(\"${PROGRAM_PREFIX}server\" SDL2::SDL2main SDL2::SDL2)\n+if(ENABLE_SDL2_NET)\n+ target_link_libraries(\"${PROGRAM_PREFIX}server\" SDL2::net)\n+endif()\n \n # Source files used by the game binaries (chocolate-doom, etc.)\n \n@@ -121,7 +124,13 @@ set(DEHACKED_SOURCE_FILES\n set(SOURCE_FILES ${COMMON_SOURCE_FILES} ${GAME_SOURCE_FILES})\n set(SOURCE_FILES_WITH_DEH ${SOURCE_FILES} ${DEHACKED_SOURCE_FILES})\n \n-set(EXTRA_LIBS SDL2::SDL2main SDL2::SDL2 SDL2::mixer SDL2::net textscreen pcsound opl)\n+set(EXTRA_LIBS SDL2::SDL2main SDL2::SDL2 textscreen pcsound opl)\n+if(ENABLE_SDL2_MIXER)\n+ list(APPEND EXTRA_LIBS SDL2::mixer)\n+endif()\n+if(ENABLE_SDL2_NET)\n+ list(APPEND EXTRA_LIBS SDL2::net)\n+endif()\n if(SAMPLERATE_FOUND)\n list(APPEND EXTRA_LIBS samplerate::samplerate)\n endif()\n@@ -213,7 +222,13 @@ endif()\n \n target_include_directories(\"${PROGRAM_PREFIX}setup\"\n PRIVATE \"${CMAKE_CURRENT_BINARY_DIR}/../\")\n-target_link_libraries(\"${PROGRAM_PREFIX}setup\" SDL2::SDL2main SDL2::SDL2 SDL2::mixer SDL2::net setup textscreen)\n+target_link_libraries(\"${PROGRAM_PREFIX}setup\" SDL2::SDL2main SDL2::SDL2 setup textscreen)\n+if(ENABLE_SDL2_mixer)\n+ target_link_libraries(\"${PROGRAM_PREFIX}setup\" SDL2::mixer)\n+endif()\n+if(ENABLE_SDL2_NET)\n+ target_link_libraries(\"${PROGRAM_PREFIX}setup\" SDL2::net)\n+endif()\n \n if(MSVC)\n set_target_properties(\"${PROGRAM_PREFIX}setup\" PROPERTIES" -+ }, -+ { -+ "sha": "82b114b4178595085c6745bc70a570922415be1f", -+ "filename": "src/doom/CMakeLists.txt", -+ "status": "modified", -+ "additions": 7, -+ "deletions": 1, -+ "changes": 8, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fdoom%2FCMakeLists.txt", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fdoom%2FCMakeLists.txt", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/src%2Fdoom%2FCMakeLists.txt?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -68,4 +68,10 @@ add_library(doom STATIC\n wi_stuff.c wi_stuff.h)\n \n target_include_directories(doom PRIVATE \"../\" \"${CMAKE_CURRENT_BINARY_DIR}/../../\")\n-target_link_libraries(doom SDL2::SDL2 SDL2::mixer SDL2::net)\n+target_link_libraries(doom SDL2::SDL2)\n+if(ENABLE_SDL2_mixer)\n+ target_link_libraries(doom SDL2::mixer)\n+endif()\n+if(ENABLE_SDL2_NET)\n+ target_link_libraries(doom SDL2::net)\n+endif()" -+ }, -+ { -+ "sha": "1ea060bfdb8b3147e36b3431fee27934c8a93b34", -+ "filename": "src/heretic/CMakeLists.txt", -+ "status": "modified", -+ "additions": 7, -+ "deletions": 1, -+ "changes": 8, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fheretic%2FCMakeLists.txt", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fheretic%2FCMakeLists.txt", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/src%2Fheretic%2FCMakeLists.txt?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -54,4 +54,10 @@ add_library(heretic STATIC\n s_sound.c s_sound.h)\n \n target_include_directories(heretic PRIVATE \"../\" \"${CMAKE_CURRENT_BINARY_DIR}/../../\")\n-target_link_libraries(heretic textscreen SDL2::SDL2 SDL2::mixer SDL2::net)\n+target_link_libraries(heretic textscreen SDL2::SDL2)\n+if(ENABLE_SDL2_mixer)\n+ target_link_libraries(heretic SDL2::mixer)\n+endif()\n+if(ENABLE_SDL2_NET)\n+ target_link_libraries(heretic SDL2::net)\n+endif()" -+ }, -+ { -+ "sha": "0dbd170bfdb06c8209ec654d4d5377e334be886e", -+ "filename": "src/hexen/CMakeLists.txt", -+ "status": "modified", -+ "additions": 7, -+ "deletions": 1, -+ "changes": 8, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fhexen%2FCMakeLists.txt", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fhexen%2FCMakeLists.txt", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/src%2Fhexen%2FCMakeLists.txt?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -55,4 +55,10 @@ add_library(hexen STATIC\n xddefs.h)\n \n target_include_directories(hexen PRIVATE \"../\" \"${CMAKE_CURRENT_BINARY_DIR}/../../\")\n-target_link_libraries(hexen SDL2::SDL2 SDL2::mixer SDL2::net)\n+target_link_libraries(hexen SDL2::SDL2)\n+if(ENABLE_SDL2_mixer)\n+ target_link_libraries(hexen SDL2::mixer)\n+endif()\n+if(ENABLE_SDL2_NET)\n+ target_link_libraries(hexen SDL2::net)\n+endif()" -+ }, -+ { -+ "sha": "3facce6f01d02c5e8eef3088fa60576b9e949829", -+ "filename": "src/i_musicpack.c", -+ "status": "modified", -+ "additions": 87, -+ "deletions": 1, -+ "changes": 88, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fi_musicpack.c", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fi_musicpack.c", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/src%2Fi_musicpack.c?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -44,6 +44,13 @@\n #include \"w_wad.h\"\n #include \"z_zone.h\"\n \n+\n+char *music_pack_path = \"\";\n+\n+\n+#ifndef DISABLE_SDL2MIXER\n+\n+\n #define MID_HEADER_MAGIC \"MThd\"\n #define MUS_HEADER_MAGIC \"MUS\\x1a\"\n \n@@ -99,7 +106,6 @@ static boolean music_initialized = false;\n \n static boolean sdl_was_initialized = false;\n \n-char *music_pack_path = \"\";\n \n // If true, we are playing a substitute digital track rather than in-WAD\n // MIDI/MUS track, and file_metadata contains loop metadata.\n@@ -1375,3 +1381,83 @@ music_module_t music_pack_module =\n I_MP_PollMusic,\n };\n \n+\n+#else // DISABLE_SDL2MIXER\n+\n+\n+static boolean I_NULL_InitMusic(void)\n+{\n+ return false;\n+}\n+\n+\n+static void I_NULL_ShutdownMusic(void)\n+{\n+}\n+\n+\n+static void I_NULL_SetMusicVolume(int volume)\n+{\n+}\n+\n+\n+static void I_NULL_PauseSong(void)\n+{\n+}\n+\n+\n+static void I_NULL_ResumeSong(void)\n+{\n+}\n+\n+\n+static void *I_NULL_RegisterSong(void *data, int len)\n+{\n+ return NULL;\n+}\n+\n+\n+static void I_NULL_UnRegisterSong(void *handle)\n+{\n+}\n+\n+\n+static void I_NULL_PlaySong(void *handle, boolean looping)\n+{\n+}\n+\n+\n+static void I_NULL_StopSong(void)\n+{\n+}\n+\n+\n+static boolean I_NULL_MusicIsPlaying(void)\n+{\n+ return false;\n+}\n+\n+\n+static void I_NULL_PollMusic(void)\n+{\n+}\n+\n+music_module_t music_pack_module =\n+{\n+ NULL,\n+ 0,\n+ I_NULL_InitMusic,\n+ I_NULL_ShutdownMusic,\n+ I_NULL_SetMusicVolume,\n+ I_NULL_PauseSong,\n+ I_NULL_ResumeSong,\n+ I_NULL_RegisterSong,\n+ I_NULL_UnRegisterSong,\n+ I_NULL_PlaySong,\n+ I_NULL_StopSong,\n+ I_NULL_MusicIsPlaying,\n+ I_NULL_PollMusic,\n+};\n+\n+\n+#endif // DISABLE_SDL2MIXER" -+ }, -+ { -+ "sha": "48fedfbd7608ad8c6575a4bd82a5e1cb3646e071", -+ "filename": "src/i_sdlmusic.c", -+ "status": "modified", -+ "additions": 21, -+ "deletions": 13, -+ "changes": 34, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fi_sdlmusic.c", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fi_sdlmusic.c", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/src%2Fi_sdlmusic.c?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -44,19 +44,6 @@\n #include \"w_wad.h\"\n #include \"z_zone.h\"\n \n-#define MAXMIDLENGTH (96 * 1024)\n-\n-static boolean music_initialized = false;\n-\n-// If this is true, this module initialized SDL sound and has the \n-// responsibility to shut it down\n-\n-static boolean sdl_was_initialized = false;\n-\n-static boolean win_midi_stream_opened = false;\n-\n-static boolean musicpaused = false;\n-static int current_music_volume;\n \n char *fluidsynth_sf_path = \"\";\n char *timidity_cfg_path = \"\";\n@@ -138,6 +125,25 @@ void I_InitTimidityConfig(void)\n }\n }\n \n+\n+#ifndef DISABLE_SDL2MIXER\n+\n+\n+#define MAXMIDLENGTH (96 * 1024)\n+\n+static boolean music_initialized = false;\n+\n+// If this is true, this module initialized SDL sound and has the\n+// responsibility to shut it down\n+\n+static boolean sdl_was_initialized = false;\n+\n+static boolean win_midi_stream_opened = false;\n+\n+static boolean musicpaused = false;\n+static int current_music_volume;\n+\n+\n // Remove the temporary config file generated by I_InitTimidityConfig().\n \n static void RemoveTimidityConfig(void)\n@@ -588,3 +594,5 @@ music_module_t music_sdl_module =\n NULL, // Poll\n };\n \n+\n+#endif // DISABLE_SDL2MIXER" -+ }, -+ { -+ "sha": "7f2a26096b218c5323f5dc569c6f70ab399782bf", -+ "filename": "src/i_sdlsound.c", -+ "status": "modified", -+ "additions": 17, -+ "deletions": 9, -+ "changes": 26, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fi_sdlsound.c", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fi_sdlsound.c", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/src%2Fi_sdlsound.c?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -41,6 +41,21 @@\n \n #include \"doomtype.h\"\n \n+\n+int use_libsamplerate = 0;\n+\n+// Scale factor used when converting libsamplerate floating point numbers\n+// to integers. Too high means the sounds can clip; too low means they\n+// will be too quiet. This is an amount that should avoid clipping most\n+// of the time: with all the Doom IWAD sound effects, at least. If a PWAD\n+// is used, clipping might occur.\n+\n+float libsamplerate_scale = 0.65f;\n+\n+\n+#ifndef DISABLE_SDL2MIXER\n+\n+\n #define LOW_PASS_FILTER\n //#define DEBUG_DUMP_WAVS\n #define NUM_CHANNELS 16\n@@ -77,15 +92,6 @@ static allocated_sound_t *allocated_sounds_head = NULL;\n static allocated_sound_t *allocated_sounds_tail = NULL;\n static int allocated_sounds_size = 0;\n \n-int use_libsamplerate = 0;\n-\n-// Scale factor used when converting libsamplerate floating point numbers\n-// to integers. Too high means the sounds can clip; too low means they\n-// will be too quiet. This is an amount that should avoid clipping most\n-// of the time: with all the Doom IWAD sound effects, at least. If a PWAD\n-// is used, clipping might occur.\n-\n-float libsamplerate_scale = 0.65f;\n \n // Hook a sound into the linked list at the head.\n \n@@ -1135,3 +1141,5 @@ sound_module_t sound_sdl_module =\n I_SDL_PrecacheSounds,\n };\n \n+\n+#endif // DISABLE_SDL2MIXER" -+ }, -+ { -+ "sha": "9cf1fd95db13504ffc6f98ead8c9150f03db72aa", -+ "filename": "src/i_sound.c", -+ "status": "modified", -+ "additions": 4, -+ "deletions": 0, -+ "changes": 4, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fi_sound.c", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fi_sound.c", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/src%2Fi_sound.c?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -99,7 +99,9 @@ static int snd_mport = 0;\n \n static sound_module_t *sound_modules[] = \n {\n+#ifndef DISABLE_SDL2MIXER\n &sound_sdl_module,\n+#endif // DISABLE_SDL2MIXER\n &sound_pcsound_module,\n NULL,\n };\n@@ -108,7 +110,9 @@ static sound_module_t *sound_modules[] =\n \n static music_module_t *music_modules[] =\n {\n+#ifndef DISABLE_SDL2MIXER\n &music_sdl_module,\n+#endif // DISABLE_SDL2MIXER\n &music_opl_module,\n NULL,\n };" -+ }, -+ { -+ "sha": "c1f701c0b9f653eb3a36b71627747a6eda963d8f", -+ "filename": "src/net_sdl.c", -+ "status": "modified", -+ "additions": 63, -+ "deletions": 0, -+ "changes": 63, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fnet_sdl.c", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fnet_sdl.c", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/src%2Fnet_sdl.c?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -33,6 +33,10 @@\n // NETWORKING\n //\n \n+\n+#ifndef DISABLE_SDL2NET\n+\n+\n #include \n \n #define DEFAULT_PORT 2342\n@@ -376,3 +380,62 @@ net_module_t net_sdl_module =\n NET_SDL_ResolveAddress,\n };\n \n+\n+#else // DISABLE_SDL2NET\n+\n+// no-op implementation\n+\n+\n+static boolean NET_NULL_InitClient(void)\n+{\n+ return false;\n+}\n+\n+\n+static boolean NET_NULL_InitServer(void)\n+{\n+ return false;\n+}\n+\n+\n+static void NET_NULL_SendPacket(net_addr_t *addr, net_packet_t *packet)\n+{\n+}\n+\n+\n+static boolean NET_NULL_RecvPacket(net_addr_t **addr, net_packet_t **packet)\n+{\n+ return false;\n+}\n+\n+\n+static void NET_NULL_AddrToString(net_addr_t *addr, char *buffer, int buffer_len)\n+{\n+\n+}\n+\n+\n+static void NET_NULL_FreeAddress(net_addr_t *addr)\n+{\n+}\n+\n+\n+net_addr_t *NET_NULL_ResolveAddress(const char *address)\n+{\n+ return NULL;\n+}\n+\n+\n+net_module_t net_sdl_module =\n+{\n+ NET_NULL_InitClient,\n+ NET_NULL_InitServer,\n+ NET_NULL_SendPacket,\n+ NET_NULL_RecvPacket,\n+ NET_NULL_AddrToString,\n+ NET_NULL_FreeAddress,\n+ NET_NULL_ResolveAddress,\n+};\n+\n+\n+#endif // DISABLE_SDL2NET" -+ }, -+ { -+ "sha": "90df2114163152ff732a9ba4a8bc18ef3d45d1bd", -+ "filename": "src/setup/CMakeLists.txt", -+ "status": "modified", -+ "additions": 4, -+ "deletions": 1, -+ "changes": 5, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fsetup%2FCMakeLists.txt", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fsetup%2FCMakeLists.txt", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/src%2Fsetup%2FCMakeLists.txt?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -15,4 +15,7 @@ add_library(setup STATIC\n txt_mouseinput.c txt_mouseinput.h)\n \n target_include_directories(setup PRIVATE \"../\" \"${CMAKE_CURRENT_BINARY_DIR}/../../\")\n-target_link_libraries(setup textscreen SDL2::SDL2 SDL2::mixer)\n+target_link_libraries(setup textscreen SDL2::SDL2)\n+if(ENABLE_SDL2_mixer)\n+ target_link_libraries(setup SDL2::mixer)\n+endif()" -+ }, -+ { -+ "sha": "37b17ade983155369c0897c0935d40dfeb2048c7", -+ "filename": "src/strife/CMakeLists.txt", -+ "status": "modified", -+ "additions": 7, -+ "deletions": 1, -+ "changes": 8, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fstrife%2FCMakeLists.txt", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fstrife%2FCMakeLists.txt", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/src%2Fstrife%2FCMakeLists.txt?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -70,4 +70,10 @@ set(STRIFE_SOURCES\n add_library(strife STATIC ${STRIFE_SOURCES})\n \n target_include_directories(strife PRIVATE \"../\" \"../../win32/\" \"${CMAKE_CURRENT_BINARY_DIR}/../../\")\n-target_link_libraries(strife textscreen SDL2::SDL2 SDL2::mixer SDL2::net)\n+target_link_libraries(strife textscreen SDL2::SDL2)\n+if(ENABLE_SDL2_mixer)\n+ target_link_libraries(strife SDL2::mixer)\n+endif()\n+if(ENABLE_SDL2_NET)\n+ target_link_libraries(strife SDL2::net)\n+endif()" -+ } -+] -diff --git a/tests/capture_tools_output/expected_result.json b/tests/capture_tools_output/expected_result.json -new file mode 100644 -index 0000000..0912b96 ---- /dev/null -+++ b/tests/capture_tools_output/expected_result.json -@@ -0,0 +1,320 @@ -+[ -+ { -+ "sha": "46e082cf77c5e60affc7689f7357c926fc00cda3", -+ "filename": "opl/opl.c", -+ "status": "modified", -+ "additions": 2, -+ "deletions": 0, -+ "changes": 2, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/opl%2Fopl.c", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/opl%2Fopl.c", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/opl%2Fopl.c?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -50,7 +50,9 @@ static opl_driver_t *drivers[] =\n #ifdef _WIN32\n &opl_win32_driver,\n #endif\n+#ifndef DISABLE_SDL2MIXER\n &opl_sdl_driver,\n+#endif // DISABLE_SDL2MIXER\n NULL\n };\n ", -+ "line_filter": { -+ "diff_chunks": [ -+ [ -+ 50, -+ 59 -+ ] -+ ], -+ "lines_added": [ -+ [ -+ 53, -+ 54 -+ ], -+ [ -+ 55, -+ 56 -+ ] -+ ] -+ } -+ }, -+ { -+ "sha": "6bd4e7e1f3374e289d3dc19972dcb1d5379d6b03", -+ "filename": "opl/opl_sdl.c", -+ "status": "modified", -+ "additions": 6, -+ "deletions": 0, -+ "changes": 6, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/opl%2Fopl_sdl.c", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/opl%2Fopl_sdl.c", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/opl%2Fopl_sdl.c?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -33,6 +33,10 @@\n \n #include \"opl_queue.h\"\n \n+\n+#ifndef DISABLE_SDL2MIXER\n+\n+\n #define MAX_SOUND_SLICE_TIME 100 /* ms */\n \n typedef struct\n@@ -511,3 +515,5 @@ opl_driver_t opl_sdl_driver =\n OPL_SDL_AdjustCallbacks,\n };\n \n+\n+#endif // DISABLE_SDL2MIXER", -+ "line_filter": { -+ "diff_chunks": [ -+ [ -+ 33, -+ 43 -+ ], -+ [ -+ 515, -+ 520 -+ ] -+ ], -+ "lines_added": [ -+ [ -+ 36, -+ 40 -+ ], -+ [ -+ 518, -+ 520 -+ ] -+ ] -+ } -+ }, -+ { -+ "sha": "58f9b75c2affd5f1dd990aaedab79d834575bf83", -+ "filename": "pcsound/pcsound.c", -+ "status": "modified", -+ "additions": 2, -+ "deletions": 0, -+ "changes": 2, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/pcsound%2Fpcsound.c", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/pcsound%2Fpcsound.c", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/pcsound%2Fpcsound.c?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -56,7 +56,9 @@ static pcsound_driver_t *drivers[] =\n #ifdef _WIN32\n &pcsound_win32_driver,\n #endif\n+#ifndef DISABLE_SDL2MIXER\n &pcsound_sdl_driver,\n+#endif // DISABLE_SDL2MIXER\n NULL,\n };\n ", -+ "line_filter": { -+ "diff_chunks": [ -+ [ -+ 56, -+ 65 -+ ] -+ ], -+ "lines_added": [ -+ [ -+ 59, -+ 60 -+ ], -+ [ -+ 61, -+ 62 -+ ] -+ ] -+ } -+ }, -+ { -+ "sha": "e77c7b0d29de1b0c665686004dfda311c9d96719", -+ "filename": "pcsound/pcsound_sdl.c", -+ "status": "modified", -+ "additions": 6, -+ "deletions": 0, -+ "changes": 6, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/pcsound%2Fpcsound_sdl.c", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/pcsound%2Fpcsound_sdl.c", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/pcsound%2Fpcsound_sdl.c?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -24,6 +24,10 @@\n #include \"pcsound.h\"\n #include \"pcsound_internal.h\"\n \n+\n+#ifndef DISABLE_SDL2MIXER\n+\n+\n #define MAX_SOUND_SLICE_TIME 70 /* ms */\n #define SQUARE_WAVE_AMP 0x2000\n \n@@ -248,3 +252,5 @@ pcsound_driver_t pcsound_sdl_driver =\n PCSound_SDL_Shutdown,\n };\n \n+\n+#endif // DISABLE_SDL2MIXER", -+ "line_filter": { -+ "diff_chunks": [ -+ [ -+ 24, -+ 34 -+ ], -+ [ -+ 252, -+ 257 -+ ] -+ ], -+ "lines_added": [ -+ [ -+ 27, -+ 31 -+ ], -+ [ -+ 255, -+ 257 -+ ] -+ ] -+ } -+ }, -+ { -+ "sha": "3facce6f01d02c5e8eef3088fa60576b9e949829", -+ "filename": "src/i_musicpack.c", -+ "status": "modified", -+ "additions": 87, -+ "deletions": 1, -+ "changes": 88, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fi_musicpack.c", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fi_musicpack.c", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/src%2Fi_musicpack.c?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -44,6 +44,13 @@\n #include \"w_wad.h\"\n #include \"z_zone.h\"\n \n+\n+char *music_pack_path = \"\";\n+\n+\n+#ifndef DISABLE_SDL2MIXER\n+\n+\n #define MID_HEADER_MAGIC \"MThd\"\n #define MUS_HEADER_MAGIC \"MUS\\x1a\"\n \n@@ -99,7 +106,6 @@ static boolean music_initialized = false;\n \n static boolean sdl_was_initialized = false;\n \n-char *music_pack_path = \"\";\n \n // If true, we are playing a substitute digital track rather than in-WAD\n // MIDI/MUS track, and file_metadata contains loop metadata.\n@@ -1375,3 +1381,83 @@ music_module_t music_pack_module =\n I_MP_PollMusic,\n };\n \n+\n+#else // DISABLE_SDL2MIXER\n+\n+\n+static boolean I_NULL_InitMusic(void)\n+{\n+ return false;\n+}\n+\n+\n+static void I_NULL_ShutdownMusic(void)\n+{\n+}\n+\n+\n+static void I_NULL_SetMusicVolume(int volume)\n+{\n+}\n+\n+\n+static void I_NULL_PauseSong(void)\n+{\n+}\n+\n+\n+static void I_NULL_ResumeSong(void)\n+{\n+}\n+\n+\n+static void *I_NULL_RegisterSong(void *data, int len)\n+{\n+ return NULL;\n+}\n+\n+\n+static void I_NULL_UnRegisterSong(void *handle)\n+{\n+}\n+\n+\n+static void I_NULL_PlaySong(void *handle, boolean looping)\n+{\n+}\n+\n+\n+static void I_NULL_StopSong(void)\n+{\n+}\n+\n+\n+static boolean I_NULL_MusicIsPlaying(void)\n+{\n+ return false;\n+}\n+\n+\n+static void I_NULL_PollMusic(void)\n+{\n+}\n+\n+music_module_t music_pack_module =\n+{\n+ NULL,\n+ 0,\n+ I_NULL_InitMusic,\n+ I_NULL_ShutdownMusic,\n+ I_NULL_SetMusicVolume,\n+ I_NULL_PauseSong,\n+ I_NULL_ResumeSong,\n+ I_NULL_RegisterSong,\n+ I_NULL_UnRegisterSong,\n+ I_NULL_PlaySong,\n+ I_NULL_StopSong,\n+ I_NULL_MusicIsPlaying,\n+ I_NULL_PollMusic,\n+};\n+\n+\n+#endif // DISABLE_SDL2MIXER", -+ "line_filter": { -+ "diff_chunks": [ -+ [ -+ 44, -+ 57 -+ ], -+ [ -+ 106, -+ 112 -+ ], -+ [ -+ 1381, -+ 1464 -+ ] -+ ], -+ "lines_added": [ -+ [ -+ 47, -+ 54 -+ ], -+ [ -+ 1384, -+ 1464 -+ ] -+ ] -+ } -+ }, -+ { -+ "sha": "48fedfbd7608ad8c6575a4bd82a5e1cb3646e071", -+ "filename": "src/i_sdlmusic.c", -+ "status": "modified", -+ "additions": 21, -+ "deletions": 13, -+ "changes": 34, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fi_sdlmusic.c", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fi_sdlmusic.c", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/src%2Fi_sdlmusic.c?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -44,19 +44,6 @@\n #include \"w_wad.h\"\n #include \"z_zone.h\"\n \n-#define MAXMIDLENGTH (96 * 1024)\n-\n-static boolean music_initialized = false;\n-\n-// If this is true, this module initialized SDL sound and has the \n-// responsibility to shut it down\n-\n-static boolean sdl_was_initialized = false;\n-\n-static boolean win_midi_stream_opened = false;\n-\n-static boolean musicpaused = false;\n-static int current_music_volume;\n \n char *fluidsynth_sf_path = \"\";\n char *timidity_cfg_path = \"\";\n@@ -138,6 +125,25 @@ void I_InitTimidityConfig(void)\n }\n }\n \n+\n+#ifndef DISABLE_SDL2MIXER\n+\n+\n+#define MAXMIDLENGTH (96 * 1024)\n+\n+static boolean music_initialized = false;\n+\n+// If this is true, this module initialized SDL sound and has the\n+// responsibility to shut it down\n+\n+static boolean sdl_was_initialized = false;\n+\n+static boolean win_midi_stream_opened = false;\n+\n+static boolean musicpaused = false;\n+static int current_music_volume;\n+\n+\n // Remove the temporary config file generated by I_InitTimidityConfig().\n \n static void RemoveTimidityConfig(void)\n@@ -588,3 +594,5 @@ music_module_t music_sdl_module =\n NULL, // Poll\n };\n \n+\n+#endif // DISABLE_SDL2MIXER", -+ "line_filter": { -+ "diff_chunks": [ -+ [ -+ 44, -+ 50 -+ ], -+ [ -+ 125, -+ 150 -+ ], -+ [ -+ 594, -+ 599 -+ ] -+ ], -+ "lines_added": [ -+ [ -+ 128, -+ 147 -+ ], -+ [ -+ 597, -+ 599 -+ ] -+ ] -+ } -+ }, -+ { -+ "sha": "7f2a26096b218c5323f5dc569c6f70ab399782bf", -+ "filename": "src/i_sdlsound.c", -+ "status": "modified", -+ "additions": 17, -+ "deletions": 9, -+ "changes": 26, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fi_sdlsound.c", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fi_sdlsound.c", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/src%2Fi_sdlsound.c?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -41,6 +41,21 @@\n \n #include \"doomtype.h\"\n \n+\n+int use_libsamplerate = 0;\n+\n+// Scale factor used when converting libsamplerate floating point numbers\n+// to integers. Too high means the sounds can clip; too low means they\n+// will be too quiet. This is an amount that should avoid clipping most\n+// of the time: with all the Doom IWAD sound effects, at least. If a PWAD\n+// is used, clipping might occur.\n+\n+float libsamplerate_scale = 0.65f;\n+\n+\n+#ifndef DISABLE_SDL2MIXER\n+\n+\n #define LOW_PASS_FILTER\n //#define DEBUG_DUMP_WAVS\n #define NUM_CHANNELS 16\n@@ -77,15 +92,6 @@ static allocated_sound_t *allocated_sounds_head = NULL;\n static allocated_sound_t *allocated_sounds_tail = NULL;\n static int allocated_sounds_size = 0;\n \n-int use_libsamplerate = 0;\n-\n-// Scale factor used when converting libsamplerate floating point numbers\n-// to integers. Too high means the sounds can clip; too low means they\n-// will be too quiet. This is an amount that should avoid clipping most\n-// of the time: with all the Doom IWAD sound effects, at least. If a PWAD\n-// is used, clipping might occur.\n-\n-float libsamplerate_scale = 0.65f;\n \n // Hook a sound into the linked list at the head.\n \n@@ -1135,3 +1141,5 @@ sound_module_t sound_sdl_module =\n I_SDL_PrecacheSounds,\n };\n \n+\n+#endif // DISABLE_SDL2MIXER", -+ "line_filter": { -+ "diff_chunks": [ -+ [ -+ 41, -+ 62 -+ ], -+ [ -+ 92, -+ 98 -+ ], -+ [ -+ 1141, -+ 1146 -+ ] -+ ], -+ "lines_added": [ -+ [ -+ 44, -+ 59 -+ ], -+ [ -+ 1144, -+ 1146 -+ ] -+ ] -+ } -+ }, -+ { -+ "sha": "9cf1fd95db13504ffc6f98ead8c9150f03db72aa", -+ "filename": "src/i_sound.c", -+ "status": "modified", -+ "additions": 4, -+ "deletions": 0, -+ "changes": 4, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fi_sound.c", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fi_sound.c", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/src%2Fi_sound.c?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -99,7 +99,9 @@ static int snd_mport = 0;\n \n static sound_module_t *sound_modules[] = \n {\n+#ifndef DISABLE_SDL2MIXER\n &sound_sdl_module,\n+#endif // DISABLE_SDL2MIXER\n &sound_pcsound_module,\n NULL,\n };\n@@ -108,7 +110,9 @@ static sound_module_t *sound_modules[] =\n \n static music_module_t *music_modules[] =\n {\n+#ifndef DISABLE_SDL2MIXER\n &music_sdl_module,\n+#endif // DISABLE_SDL2MIXER\n &music_opl_module,\n NULL,\n };", -+ "line_filter": { -+ "diff_chunks": [ -+ [ -+ 99, -+ 108 -+ ], -+ [ -+ 110, -+ 119 -+ ] -+ ], -+ "lines_added": [ -+ [ -+ 102, -+ 103 -+ ], -+ [ -+ 104, -+ 105 -+ ], -+ [ -+ 113, -+ 114 -+ ], -+ [ -+ 115, -+ 116 -+ ] -+ ] -+ } -+ }, -+ { -+ "sha": "c1f701c0b9f653eb3a36b71627747a6eda963d8f", -+ "filename": "src/net_sdl.c", -+ "status": "modified", -+ "additions": 63, -+ "deletions": 0, -+ "changes": 63, -+ "blob_url": "https://github.com/chocolate-doom/chocolate-doom/blob/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fnet_sdl.c", -+ "raw_url": "https://github.com/chocolate-doom/chocolate-doom/raw/3c35acde7d398c32f6fad50fa902de391f573ffa/src%2Fnet_sdl.c", -+ "contents_url": "https://api.github.com/repos/chocolate-doom/chocolate-doom/contents/src%2Fnet_sdl.c?ref=3c35acde7d398c32f6fad50fa902de391f573ffa", -+ "patch": "@@ -33,6 +33,10 @@\n // NETWORKING\n //\n \n+\n+#ifndef DISABLE_SDL2NET\n+\n+\n #include \n \n #define DEFAULT_PORT 2342\n@@ -376,3 +380,62 @@ net_module_t net_sdl_module =\n NET_SDL_ResolveAddress,\n };\n \n+\n+#else // DISABLE_SDL2NET\n+\n+// no-op implementation\n+\n+\n+static boolean NET_NULL_InitClient(void)\n+{\n+ return false;\n+}\n+\n+\n+static boolean NET_NULL_InitServer(void)\n+{\n+ return false;\n+}\n+\n+\n+static void NET_NULL_SendPacket(net_addr_t *addr, net_packet_t *packet)\n+{\n+}\n+\n+\n+static boolean NET_NULL_RecvPacket(net_addr_t **addr, net_packet_t **packet)\n+{\n+ return false;\n+}\n+\n+\n+static void NET_NULL_AddrToString(net_addr_t *addr, char *buffer, int buffer_len)\n+{\n+\n+}\n+\n+\n+static void NET_NULL_FreeAddress(net_addr_t *addr)\n+{\n+}\n+\n+\n+net_addr_t *NET_NULL_ResolveAddress(const char *address)\n+{\n+ return NULL;\n+}\n+\n+\n+net_module_t net_sdl_module =\n+{\n+ NET_NULL_InitClient,\n+ NET_NULL_InitServer,\n+ NET_NULL_SendPacket,\n+ NET_NULL_RecvPacket,\n+ NET_NULL_AddrToString,\n+ NET_NULL_FreeAddress,\n+ NET_NULL_ResolveAddress,\n+};\n+\n+\n+#endif // DISABLE_SDL2NET", -+ "line_filter": { -+ "diff_chunks": [ -+ [ -+ 33, -+ 43 -+ ], -+ [ -+ 380, -+ 442 -+ ] -+ ], -+ "lines_added": [ -+ [ -+ 36, -+ 40 -+ ], -+ [ -+ 383, -+ 442 -+ ] -+ ] -+ } -+ } -+] -diff --git a/tests/capture_tools_output/test_database_path.py b/tests/capture_tools_output/test_database_path.py -new file mode 100644 -index 0000000..3bafa66 ---- /dev/null -+++ b/tests/capture_tools_output/test_database_path.py -@@ -0,0 +1,59 @@ -+"""Tests specific to specifying the compilation database path.""" -+from typing import List -+from pathlib import Path, PurePath -+import logging -+import re -+import pytest -+from cpp_linter import logger -+import cpp_linter.run -+from cpp_linter.run import run_clang_tidy -+ -+CLANG_TIDY_COMMAND = re.compile(r"\"clang-tidy(.*)(?:\")") -+ -+ABS_DB_PATH = str(Path("tests/demo").resolve()) -+ -+ -+@pytest.mark.parametrize( -+ "database,expected_args", -+ [ -+ # implicit path to the compilation database -+ ("", []), -+ # explicit relative path to the compilation database -+ ("../../demo", ["-p", ABS_DB_PATH]), -+ # explicit absolute path to the compilation database -+ (ABS_DB_PATH, ["-p", ABS_DB_PATH]), -+ ], -+ ids=["implicit path", "relative path", "absolute path"], -+) -+def test_db_detection( -+ caplog: pytest.LogCaptureFixture, -+ monkeypatch: pytest.MonkeyPatch, -+ pytestconfig: pytest.Config, -+ database: str, -+ expected_args: List[str], -+): -+ """test clang-tidy using a implicit path to the compilation database.""" -+ monkeypatch.chdir(PurePath(__file__).parent.as_posix()) -+ demo_src = "../demo/demo.cpp" -+ rel_root = str(Path(*Path(__file__).parts[-2:])) -+ cpp_linter.run.RUNNER_WORKSPACE = ( -+ Path(pytestconfig.rootpath, "tests").resolve().as_posix() -+ ) -+ caplog.set_level(logging.DEBUG, logger=logger.name) -+ run_clang_tidy( -+ filename=(demo_src), -+ file_obj={}, # only used when filtering lines -+ version="", -+ checks="", # let clang-tidy use a .clang-tidy config file -+ lines_changed_only=0, # analyze complete file -+ database=database, -+ repo_root=rel_root, -+ ) -+ matched_args = [] -+ for record in caplog.records: -+ msg_match = CLANG_TIDY_COMMAND.search(record.message) -+ if msg_match is not None: -+ matched_args = msg_match.group(0)[:-1].split()[2:] -+ assert "Error while trying to load a compilation database" not in record.message -+ expected_args.append(demo_src) -+ assert matched_args == expected_args -diff --git a/tests/capture_tools_output/test_tools_output.py b/tests/capture_tools_output/test_tools_output.py -new file mode 100644 -index 0000000..c7467fa ---- /dev/null -+++ b/tests/capture_tools_output/test_tools_output.py -@@ -0,0 +1,207 @@ -+"""Various tests related to the ``lines_changed_only`` option.""" -+import os -+import logging -+from typing import Dict, Any, cast, List, Optional -+from pathlib import Path -+import json -+import re -+import pytest -+import cpp_linter -+from cpp_linter.run import ( -+ filter_out_non_source_files, -+ verify_files_are_present, -+ capture_clang_tools_output, -+ make_annotations, -+ log_commander, -+) -+from cpp_linter.thread_comments import list_diff_comments -+ -+CLANG_VERSION = os.getenv("CLANG_VERSION", "12") -+ -+ -+@pytest.mark.parametrize( -+ "extensions", [(["c"]), pytest.param(["h"], marks=pytest.mark.xfail)] -+) -+def test_lines_changed_only( -+ monkeypatch: pytest.MonkeyPatch, -+ caplog: pytest.LogCaptureFixture, -+ extensions: List[str], -+): -+ """Test for lines changes in diff. -+ -+ This checks for -+ 1. ranges of diff chunks. -+ 2. ranges of lines in diff that only contain additions. -+ """ -+ monkeypatch.chdir(str(Path(__file__).parent)) -+ caplog.set_level(logging.DEBUG, logger=cpp_linter.logger.name) -+ cpp_linter.Globals.FILES = json.loads( -+ Path("event_files.json").read_text(encoding="utf-8") -+ ) -+ if filter_out_non_source_files( -+ ext_list=extensions, -+ ignored=[".github"], -+ not_ignored=[], -+ ): -+ test_result = Path("expected_result.json").read_text(encoding="utf-8") -+ for file, result in zip( -+ cpp_linter.Globals.FILES, -+ json.loads(test_result), -+ ): -+ expected = result["line_filter"]["diff_chunks"] -+ assert file["line_filter"]["diff_chunks"] == expected -+ expected = result["line_filter"]["lines_added"] -+ assert file["line_filter"]["lines_added"] == expected -+ else: -+ raise RuntimeError("test failed to find files") -+ -+ -+TEST_REPO = re.compile(r".*github.com/(?:\w|\-|_)+/((?:\w|\-|_)+)/.*") -+ -+ -+@pytest.fixture(autouse=True) -+def setup_test_repo(monkeypatch: pytest.MonkeyPatch) -> None: -+ """Setup a test repo to run the rest of the tests in this module.""" -+ test_root = Path(__file__).parent -+ cpp_linter.Globals.FILES = json.loads( -+ Path(test_root / "expected_result.json").read_text(encoding="utf-8") -+ ) -+ # flush output from any previous tests -+ cpp_linter.Globals.OUTPUT = "" -+ cpp_linter.GlobalParser.format_advice = [] -+ cpp_linter.GlobalParser.tidy_notes = [] -+ cpp_linter.GlobalParser.tidy_advice = [] -+ -+ repo_root = TEST_REPO.sub("\\1", cpp_linter.Globals.FILES[0]["blob_url"]) -+ return_path = test_root / repo_root -+ if not return_path.exists(): -+ return_path.mkdir() -+ monkeypatch.chdir(str(return_path)) -+ verify_files_are_present() -+ -+ -+def match_file_json(filename: str) -> Optional[Dict[str, Any]]: -+ """A helper function to match a given filename with a file's JSON object.""" -+ for file in cpp_linter.Globals.FILES: -+ if file["filename"] == filename: -+ return file -+ print("file", filename, "not found in expected_result.json") -+ return None -+ -+ -+RECORD_FILE = re.compile(r".*file=(.*?),.*") -+FORMAT_RECORD = re.compile(r"Run clang-format on ") -+FORMAT_RECORD_LINES = re.compile(r".*\(lines (.*)\).*") -+TIDY_RECORD = re.compile(r":\d+:\d+ \[.*\]::") -+TIDY_RECORD_LINE = re.compile(r".*,line=(\d+).*") -+ -+ -+@pytest.mark.parametrize( -+ "lines_changed_only", [0, 1, 2], ids=["all lines", "only diff", "only added"] -+) -+@pytest.mark.parametrize("style", ["file", "llvm", "google"]) -+def test_format_annotations( -+ caplog: pytest.LogCaptureFixture, -+ lines_changed_only: int, -+ style: str, -+): -+ """Test clang-format annotations.""" -+ capture_clang_tools_output( -+ version=CLANG_VERSION, -+ checks="-*", # disable clang-tidy output -+ style=style, -+ lines_changed_only=lines_changed_only, -+ database="", -+ repo_root="", -+ ) -+ assert "Output from `clang-tidy`" not in cpp_linter.Globals.OUTPUT -+ caplog.set_level(logging.INFO, logger=log_commander.name) -+ log_commander.propagate = True -+ make_annotations( -+ style=style, file_annotations=True, lines_changed_only=lines_changed_only -+ ) -+ for message in [r.message for r in caplog.records if r.levelno == logging.INFO]: -+ if FORMAT_RECORD.search(message) is not None: -+ lines = [ -+ int(l.strip()) -+ for l in FORMAT_RECORD_LINES.sub("\\1", message).split(",") -+ ] -+ file = match_file_json(RECORD_FILE.sub("\\1", message).replace("\\", "/")) -+ if file is None: -+ continue -+ ranges = cpp_linter.range_of_changed_lines(file, lines_changed_only) -+ if ranges: # an empty list if lines_changed_only == 0 -+ for line in lines: -+ assert line in ranges -+ else: -+ raise RuntimeWarning(f"unrecognized record: {message}") -+ -+ -+@pytest.mark.parametrize( -+ "lines_changed_only", [0, 1, 2], ids=["all lines", "only diff", "only added"] -+) -+@pytest.mark.parametrize( -+ "checks", -+ [ -+ "", -+ "boost-*,bugprone-*,performance-*,readability-*,portability-*,modernize-*," -+ "clang-analyzer-*,cppcoreguidelines-*", -+ ], -+ ids=["config file", "action defaults"], -+) -+def test_tidy_annotations( -+ caplog: pytest.LogCaptureFixture, -+ lines_changed_only: int, -+ checks: str, -+): -+ """Test clang-tidy annotations.""" -+ capture_clang_tools_output( -+ version=CLANG_VERSION, -+ checks=checks, -+ style="", # disable clang-format output -+ lines_changed_only=lines_changed_only, -+ database="", -+ repo_root="", -+ ) -+ assert "Run `clang-format` on the following files" not in cpp_linter.Globals.OUTPUT -+ caplog.set_level(logging.INFO, logger=log_commander.name) -+ log_commander.propagate = True -+ make_annotations( -+ style="", file_annotations=True, lines_changed_only=lines_changed_only -+ ) -+ for message in [r.message for r in caplog.records if r.levelno == logging.INFO]: -+ if TIDY_RECORD.search(message) is not None: -+ line = int(TIDY_RECORD_LINE.sub("\\1", message)) -+ file = match_file_json(RECORD_FILE.sub("\\1", message).replace("\\", "/")) -+ if file is None: -+ continue -+ ranges = cpp_linter.range_of_changed_lines(file, lines_changed_only) -+ if ranges: # an empty list if lines_changed_only == 0 -+ assert line in ranges -+ else: -+ raise RuntimeWarning(f"unrecognized record: {message}") -+ -+ -+@pytest.mark.parametrize("lines_changed_only", [1, 2], ids=["only diff", "only added"]) -+def test_diff_comment(lines_changed_only: int): -+ """Tests code that isn't actually used (yet) for posting -+ comments (not annotations) in the event's diff. -+ -+ Remember, diff comments should only focus on lines in the diff.""" -+ capture_clang_tools_output( -+ version=CLANG_VERSION, -+ checks="", -+ style="file", -+ lines_changed_only=lines_changed_only, -+ database="", -+ repo_root="", -+ ) -+ diff_comments = list_diff_comments(lines_changed_only) -+ # output = Path(__file__).parent / "diff_comments.json" -+ # output.write_text(json.dumps(diff_comments, indent=2), encoding="utf-8") -+ for comment in diff_comments: -+ file = match_file_json(cast(str, comment["path"])) -+ if file is None: -+ continue -+ ranges = cpp_linter.range_of_changed_lines(file, lines_changed_only) -+ assert comment["line"] in ranges -diff --git a/tests/demo/.clang-format b/tests/demo/.clang-format -new file mode 100644 -index 0000000..1dd236c ---- /dev/null -+++ b/tests/demo/.clang-format -@@ -0,0 +1,3 @@ -+--- -+Language: Cpp -+BasedOnStyle: WebKit -diff --git a/tests/demo/.clang-tidy b/tests/demo/.clang-tidy -new file mode 100644 -index 0000000..d3865ad ---- /dev/null -+++ b/tests/demo/.clang-tidy -@@ -0,0 +1,186 @@ -+--- -+Checks: 'clang-diagnostic-*,clang-analyzer-*,-*,performance-*,bugprone-*,clang-analyzer-*,mpi-*,misc-*,readability-*' -+WarningsAsErrors: '' -+HeaderFilterRegex: '' -+AnalyzeTemporaryDtors: false -+FormatStyle: 'file' -+CheckOptions: -+ - key: bugprone-argument-comment.CommentBoolLiterals -+ value: '0' -+ - key: bugprone-argument-comment.CommentCharacterLiterals -+ value: '0' -+ - key: bugprone-argument-comment.CommentFloatLiterals -+ value: '0' -+ - key: bugprone-argument-comment.CommentIntegerLiterals -+ value: '0' -+ - key: bugprone-argument-comment.CommentNullPtrs -+ value: '0' -+ - key: bugprone-argument-comment.CommentStringLiterals -+ value: '0' -+ - key: bugprone-argument-comment.CommentUserDefinedLiterals -+ value: '0' -+ - key: bugprone-argument-comment.IgnoreSingleArgument -+ value: '0' -+ - key: bugprone-argument-comment.StrictMode -+ value: '0' -+ - key: bugprone-assert-side-effect.AssertMacros -+ value: assert -+ - key: bugprone-assert-side-effect.CheckFunctionCalls -+ value: '0' -+ - key: bugprone-dangling-handle.HandleClasses -+ value: 'std::basic_string_view;std::experimental::basic_string_view' -+ - key: bugprone-dynamic-static-initializers.HeaderFileExtensions -+ value: ',h,hh,hpp,hxx' -+ - key: bugprone-exception-escape.FunctionsThatShouldNotThrow -+ value: '' -+ - key: bugprone-exception-escape.IgnoredExceptions -+ value: '' -+ - key: bugprone-misplaced-widening-cast.CheckImplicitCasts -+ value: '0' -+ - key: bugprone-not-null-terminated-result.WantToUseSafeFunctions -+ value: '1' -+ - key: bugprone-signed-char-misuse.CharTypdefsToIgnore -+ value: '' -+ - key: bugprone-sizeof-expression.WarnOnSizeOfCompareToConstant -+ value: '1' -+ - key: bugprone-sizeof-expression.WarnOnSizeOfConstant -+ value: '1' -+ - key: bugprone-sizeof-expression.WarnOnSizeOfIntegerExpression -+ value: '0' -+ - key: bugprone-sizeof-expression.WarnOnSizeOfThis -+ value: '1' -+ - key: bugprone-string-constructor.LargeLengthThreshold -+ value: '8388608' -+ - key: bugprone-string-constructor.WarnOnLargeLength -+ value: '1' -+ - key: bugprone-suspicious-enum-usage.StrictMode -+ value: '0' -+ - key: bugprone-suspicious-missing-comma.MaxConcatenatedTokens -+ value: '5' -+ - key: bugprone-suspicious-missing-comma.RatioThreshold -+ value: '0.200000' -+ - key: bugprone-suspicious-missing-comma.SizeThreshold -+ value: '5' -+ - key: bugprone-suspicious-string-compare.StringCompareLikeFunctions -+ value: '' -+ - key: bugprone-suspicious-string-compare.WarnOnImplicitComparison -+ value: '1' -+ - key: bugprone-suspicious-string-compare.WarnOnLogicalNotComparison -+ value: '0' -+ - key: bugprone-too-small-loop-variable.MagnitudeBitsUpperLimit -+ value: '16' -+ - key: bugprone-unhandled-self-assignment.WarnOnlyIfThisHasSuspiciousField -+ value: '1' -+ - key: bugprone-unused-return-value.CheckedFunctions -+ value: '::std::async;::std::launder;::std::remove;::std::remove_if;::std::unique;::std::unique_ptr::release;::std::basic_string::empty;::std::vector::empty' -+ - key: cert-dcl16-c.NewSuffixes -+ value: 'L;LL;LU;LLU' -+ - key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField -+ value: '0' -+ - key: cppcoreguidelines-explicit-virtual-functions.IgnoreDestructors -+ value: '1' -+ - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic -+ value: '1' -+ - key: google-readability-braces-around-statements.ShortStatementLines -+ value: '1' -+ - key: google-readability-function-size.StatementThreshold -+ value: '800' -+ - key: google-readability-namespace-comments.ShortNamespaceLines -+ value: '10' -+ - key: google-readability-namespace-comments.SpacesBeforeComments -+ value: '2' -+ - key: misc-definitions-in-headers.HeaderFileExtensions -+ value: ',h,hh,hpp,hxx' -+ - key: misc-definitions-in-headers.UseHeaderFileExtension -+ value: '1' -+ - key: misc-throw-by-value-catch-by-reference.CheckThrowTemporaries -+ value: '1' -+ - key: misc-unused-parameters.StrictMode -+ value: '0' -+ - key: modernize-loop-convert.MaxCopySize -+ value: '16' -+ - key: modernize-loop-convert.MinConfidence -+ value: reasonable -+ - key: modernize-loop-convert.NamingStyle -+ value: CamelCase -+ - key: modernize-pass-by-value.IncludeStyle -+ value: llvm -+ - key: modernize-replace-auto-ptr.IncludeStyle -+ value: llvm -+ - key: modernize-use-nullptr.NullMacros -+ value: 'NULL' -+ - key: performance-faster-string-find.StringLikeClasses -+ value: 'std::basic_string' -+ - key: performance-for-range-copy.AllowedTypes -+ value: '' -+ - key: performance-for-range-copy.WarnOnAllAutoCopies -+ value: '0' -+ - key: performance-inefficient-string-concatenation.StrictMode -+ value: '0' -+ - key: performance-inefficient-vector-operation.EnableProto -+ value: '0' -+ - key: performance-inefficient-vector-operation.VectorLikeClasses -+ value: '::std::vector' -+ - key: performance-move-const-arg.CheckTriviallyCopyableMove -+ value: '1' -+ - key: performance-move-constructor-init.IncludeStyle -+ value: llvm -+ - key: performance-no-automatic-move.AllowedTypes -+ value: '' -+ - key: performance-type-promotion-in-math-fn.IncludeStyle -+ value: llvm -+ - key: performance-unnecessary-copy-initialization.AllowedTypes -+ value: '' -+ - key: performance-unnecessary-value-param.AllowedTypes -+ value: '' -+ - key: performance-unnecessary-value-param.IncludeStyle -+ value: llvm -+ - key: readability-braces-around-statements.ShortStatementLines -+ value: '0' -+ - key: readability-else-after-return.WarnOnUnfixable -+ value: '1' -+ - key: readability-function-size.BranchThreshold -+ value: '4294967295' -+ - key: readability-function-size.LineThreshold -+ value: '4294967295' -+ - key: readability-function-size.NestingThreshold -+ value: '4294967295' -+ - key: readability-function-size.ParameterThreshold -+ value: '4294967295' -+ - key: readability-function-size.StatementThreshold -+ value: '800' -+ - key: readability-function-size.VariableThreshold -+ value: '4294967295' -+ - key: readability-identifier-naming.IgnoreFailedSplit -+ value: '0' -+ - key: readability-implicit-bool-conversion.AllowIntegerConditions -+ value: '0' -+ - key: readability-implicit-bool-conversion.AllowPointerConditions -+ value: '0' -+ - key: readability-inconsistent-declaration-parameter-name.IgnoreMacros -+ value: '1' -+ - key: readability-inconsistent-declaration-parameter-name.Strict -+ value: '0' -+ - key: readability-magic-numbers.IgnoredFloatingPointValues -+ value: '1.0;100.0;' -+ - key: readability-magic-numbers.IgnoredIntegerValues -+ value: '1;2;3;4;' -+ - key: readability-redundant-member-init.IgnoreBaseInCopyConstructors -+ value: '0' -+ - key: readability-redundant-smartptr-get.IgnoreMacros -+ value: '1' -+ - key: readability-redundant-string-init.StringNames -+ value: '::std::basic_string' -+ - key: readability-simplify-boolean-expr.ChainedConditionalAssignment -+ value: '0' -+ - key: readability-simplify-boolean-expr.ChainedConditionalReturn -+ value: '0' -+ - key: readability-simplify-subscript-expr.Types -+ value: '::std::basic_string;::std::basic_string_view;::std::vector;::std::array' -+ - key: readability-static-accessed-through-instance.NameSpecifierNestingThreshold -+ value: '3' -+ - key: readability-uppercase-literal-suffix.IgnoreMacros -+ value: '1' -+ - key: readability-uppercase-literal-suffix.NewSuffixes -+ value: '' -+... -diff --git a/tests/demo/compile_flags.txt b/tests/demo/compile_flags.txt -new file mode 100644 -index 0000000..03e4446 ---- /dev/null -+++ b/tests/demo/compile_flags.txt -@@ -0,0 +1,2 @@ -+-Wall -+-Werror -diff --git a/tests/demo/demo.cpp b/tests/demo/demo.cpp -new file mode 100644 -index 0000000..1bf553e ---- /dev/null -+++ b/tests/demo/demo.cpp -@@ -0,0 +1,18 @@ -+/** This is a very ugly test code (doomed to fail linting) */ -+#include "demo.hpp" -+#include -+ -+ -+ -+ -+int main(){ -+ -+ for (;;) break; -+ -+ -+ printf("Hello world!\n"); -+ -+ -+ -+ -+ return 0;} -diff --git a/tests/demo/demo.hpp b/tests/demo/demo.hpp -new file mode 100644 -index 0000000..f93d012 ---- /dev/null -+++ b/tests/demo/demo.hpp -@@ -0,0 +1,36 @@ -+#pragma once -+ -+ -+ -+class Dummy { -+ char* useless; -+ int numb; -+ Dummy() :numb(0), useless("\0"){} -+ -+ public: -+ void *not_useful(char *str){useless = str;} -+}; -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+struct LongDiff -+{ -+ -+ long diff; -+ -+}; -diff --git a/tests/ignored_paths/.gitmodules b/tests/ignored_paths/.gitmodules -new file mode 100644 -index 0000000..3696a31 ---- /dev/null -+++ b/tests/ignored_paths/.gitmodules -@@ -0,0 +1,12 @@ -+[submodule "RF24"] -+ path = RF24 -+ url = https://github.com/nRF24/RF24.git -+[submodule "RF24Network"] -+ path = RF24Network -+ url = https://github.com/nRF24/RF24Network.git -+[submodule "RF24Mesh"] -+ path = RF24Mesh -+ url = https://github.com/nRF24/RF24Mesh.git -+[submodule "pybind11"] -+ path = pybind11 -+ url = https://github.com/pybind/pybind11.git -diff --git a/tests/ignored_paths/test_ignored_paths.py b/tests/ignored_paths/test_ignored_paths.py -new file mode 100644 -index 0000000..b27d3b8 ---- /dev/null -+++ b/tests/ignored_paths/test_ignored_paths.py -@@ -0,0 +1,32 @@ -+"""Tests that focus on the ``ignore`` option's parsing.""" -+from pathlib import Path -+from typing import List -+import pytest -+from cpp_linter.run import parse_ignore_option, is_file_in_list -+ -+ -+@pytest.mark.parametrize( -+ "user_in,is_ignored,is_not_ignored,expected", -+ [ -+ ("src", "src", "src", [True, False]), -+ ("!src|./", "", "src", [True, True]), -+ ], -+) -+def test_ignore( -+ user_in: str, is_ignored: str, is_not_ignored: str, expected: List[bool] -+): -+ """test ignoring of a specified path.""" -+ ignored, not_ignored = parse_ignore_option(user_in) -+ assert expected == [ -+ is_file_in_list(ignored, is_ignored, "ignored"), -+ is_file_in_list(not_ignored, is_not_ignored, "not ignored"), -+ ] -+ -+ -+def test_ignore_submodule(monkeypatch: pytest.MonkeyPatch): -+ """test auto detection of submodules and ignore the paths appropriately.""" -+ monkeypatch.chdir(str(Path(__file__).parent)) -+ ignored, not_ignored = parse_ignore_option("!pybind11") -+ for ignored_submodule in ["RF24", "RF24Network", "RF24Mesh"]: -+ assert ignored_submodule in ignored -+ assert "pybind11" in not_ignored -diff --git a/tests/test_cli_args.py b/tests/test_cli_args.py -new file mode 100644 -index 0000000..4015877 ---- /dev/null -+++ b/tests/test_cli_args.py -@@ -0,0 +1,75 @@ -+"""Tests related parsing input from CLI arguments.""" -+from typing import List, Union -+import pytest -+from cpp_linter.run import cli_arg_parser -+ -+ -+class Args: -+ """A pseudo namespace declaration. Each attribute is initialized with the -+ corresponding CLI arg's default value.""" -+ -+ verbosity: int = 10 -+ database: str = "" -+ style: str = "llvm" -+ tidy_checks: str = ( -+ "boost-*,bugprone-*,performance-*,readability-*,portability-*,modernize-*," -+ "clang-analyzer-*,cppcoreguidelines-*" -+ ) -+ version: str = "" -+ extensions: List[str] = [ -+ "c", -+ "h", -+ "C", -+ "H", -+ "cpp", -+ "hpp", -+ "cc", -+ "hh", -+ "c++", -+ "h++", -+ "cxx", -+ "hxx", -+ ] -+ repo_root: str = "." -+ ignore: str = ".github" -+ lines_changed_only: int = 0 -+ files_changed_only: bool = False -+ thread_comments: bool = False -+ file_annotations: bool = True -+ -+ -+def test_defaults(): -+ """test default values""" -+ args = cli_arg_parser.parse_args("") -+ for key in args.__dict__.keys(): -+ assert args.__dict__[key] == getattr(Args, key) -+ -+ -+@pytest.mark.parametrize( -+ "arg_name,arg_value,attr_name,attr_value", -+ [ -+ ("verbosity", "20", "verbosity", 20), -+ ("database", "build", "database", "build"), -+ ("style", "file", "style", "file"), -+ ("tidy-checks", "-*", "tidy_checks", "-*"), -+ ("version", "14", "version", "14"), -+ ("extensions", ".cpp, .h", "extensions", ["cpp", "h"]), -+ ("extensions", "cxx,.hpp", "extensions", ["cxx", "hpp"]), -+ ("repo-root", "src", "repo_root", "src"), -+ ("ignore", "!src|", "ignore", "!src|"), -+ ("lines-changed-only", "True", "lines_changed_only", 2), -+ ("lines-changed-only", "difF", "lines_changed_only", 1), -+ ("files-changed-only", "True", "files_changed_only", True), -+ ("thread-comments", "True", "thread_comments", True), -+ ("file-annotations", "False", "file_annotations", False), -+ ], -+) -+def test_arg_parser( -+ arg_name: str, -+ arg_value: str, -+ attr_name: str, -+ attr_value: Union[int, str, List[str], bool], -+): -+ """parameterized test of specific args compared to their parsed value""" -+ args = cli_arg_parser.parse_args([f"--{arg_name}={arg_value}"]) -+ assert getattr(args, attr_name) == attr_value -diff --git a/tests/test_misc.py b/tests/test_misc.py -new file mode 100644 -index 0000000..ce7bd26 ---- /dev/null -+++ b/tests/test_misc.py -@@ -0,0 +1,104 @@ -+"""Tests that complete coverage that aren't prone to failure.""" -+import logging -+from pathlib import Path -+from typing import List -+import pytest -+import requests -+import cpp_linter -+import cpp_linter.run -+from cpp_linter import Globals, log_response_msg, get_line_cnt_from_cols -+from cpp_linter.run import ( -+ log_commander, -+ start_log_group, -+ end_log_group, -+ set_exit_code, -+ list_source_files, -+ get_list_of_changed_files, -+) -+ -+ -+def test_exit_override(): -+ """Test exit code that indicates if action encountered lining errors.""" -+ assert 1 == set_exit_code(1) -+ -+ -+def test_exit_implicit(): -+ """Test the exit code issued when a thread comment is to be made.""" -+ Globals.OUTPUT = "TEST" # fake content for a thread comment -+ assert 1 == set_exit_code() -+ -+ -+# see https://github.com/pytest-dev/pytest/issues/5997 -+def test_end_group(caplog: pytest.LogCaptureFixture): -+ """Test the output that concludes a group of runner logs.""" -+ caplog.set_level(logging.INFO, logger=log_commander.name) -+ log_commander.propagate = True -+ end_log_group() -+ messages = caplog.messages -+ assert "::endgroup::" in messages -+ -+ -+# see https://github.com/pytest-dev/pytest/issues/5997 -+def test_start_group(caplog: pytest.LogCaptureFixture): -+ """Test the output that begins a group of runner logs.""" -+ caplog.set_level(logging.INFO, logger=log_commander.name) -+ log_commander.propagate = True -+ start_log_group("TEST") -+ messages = caplog.messages -+ assert "::group::TEST" in messages -+ -+ -+@pytest.mark.parametrize( -+ "url", -+ [ -+ ("https://api.github.com/users/cpp-linter/starred"), -+ pytest.param(("https://github.com/cpp-linter/repo"), marks=pytest.mark.xfail), -+ ], -+) -+def test_response_logs(url: str): -+ """Test the log output for a requests.response buffer.""" -+ Globals.response_buffer = requests.get(url) -+ assert log_response_msg() -+ -+ -+@pytest.mark.parametrize( -+ "extensions", -+ [ -+ (["cpp", "hpp"]), -+ pytest.param(["cxx", "h"], marks=pytest.mark.xfail), -+ ], -+) -+def test_list_src_files( -+ monkeypatch: pytest.MonkeyPatch, -+ caplog: pytest.LogCaptureFixture, -+ extensions: List[str], -+): -+ """List the source files in the demo folder of this repo.""" -+ Globals.FILES = [] -+ monkeypatch.chdir(Path(__file__).parent.parent.as_posix()) -+ caplog.set_level(logging.DEBUG, logger=cpp_linter.logger.name) -+ assert list_source_files(ext_list=extensions, ignored_paths=[], not_ignored=[]) -+ -+ -+def test_get_changed_files(caplog: pytest.LogCaptureFixture): -+ """test getting a list of changed files for an event. -+ -+ This is expected to fail if a github token not supplied as an env var. -+ We don't need to supply one for this test because the tested code will -+ execute anyway. -+ """ -+ caplog.set_level(logging.DEBUG, logger=cpp_linter.logger.name) -+ cpp_linter.run.GITHUB_REPOSITORY = "cpp-linter/test-cpp-linter-action" -+ cpp_linter.run.GITHUB_SHA = "76adde5367196cd57da5bef49a4f09af6175fd3f" -+ cpp_linter.run.GITHUB_EVENT_NAME = "push" -+ get_list_of_changed_files() -+ # pylint: disable=no-member -+ assert Globals.FILES -+ # pylint: enable=no-member -+ -+ -+@pytest.mark.parametrize("line,cols,offset", [(13, 5, 144), (19, 1, 189)]) -+def test_file_offset_translation(line: int, cols: int, offset: int): -+ """Validate output from ``get_line_cnt_from_cols()``""" -+ test_file = str(Path("tests/demo/demo.cpp").resolve()) -+ assert (line, cols) == get_line_cnt_from_cols(test_file, offset) diff --git a/cpp-linter/tests/ignored_paths/.gitmodules b/cpp-linter/tests/ignored_paths/.gitmodules deleted file mode 100644 index 3696a312..00000000 --- a/cpp-linter/tests/ignored_paths/.gitmodules +++ /dev/null @@ -1,12 +0,0 @@ -[submodule "RF24"] - path = RF24 - url = https://github.com/nRF24/RF24.git -[submodule "RF24Network"] - path = RF24Network - url = https://github.com/nRF24/RF24Network.git -[submodule "RF24Mesh"] - path = RF24Mesh - url = https://github.com/nRF24/RF24Mesh.git -[submodule "pybind11"] - path = pybind11 - url = https://github.com/pybind/pybind11.git diff --git a/cpp-linter/tests/paginated_changed_files.rs b/cpp-linter/tests/paginated_changed_files.rs index 15015480..f8b57001 100644 --- a/cpp-linter/tests/paginated_changed_files.rs +++ b/cpp-linter/tests/paginated_changed_files.rs @@ -1,15 +1,12 @@ +#![cfg(feature = "bin")] mod common; use chrono::Utc; use common::{create_test_space, mock_server}; +use git_bot_feedback::{FileFilter, LinesChangedOnly}; use mockito::Matcher; use tempfile::{NamedTempFile, TempDir}; -use cpp_linter::{ - cli::LinesChangedOnly, - common_fs::FileFilter, - logger, - rest_api::{RestApiClient, github::GithubApiClient}, -}; +use cpp_linter::{logger, rest_client::RestClient}; use std::{env, io::Write, path::Path}; #[derive(PartialEq, Default)] @@ -31,7 +28,6 @@ const REPO: &str = "cpp-linter/test-cpp-linter-action"; const SHA: &str = "DEADBEEF"; const PR: u8 = 42; const TOKEN: &str = "123456"; -const EVENT_PAYLOAD: &str = r#"{"number": 42}"#; const RESET_RATE_LIMIT_HEADER: &str = "x-ratelimit-reset"; const REMAINING_RATE_LIMIT_HEADER: &str = "x-ratelimit-remaining"; const MALFORMED_RESPONSE_PAYLOAD: &str = "{\"message\":\"Resource not accessible by integration\"}"; @@ -41,6 +37,7 @@ async fn get_paginated_changes(lib_root: &Path, test_params: &TestParams) { let mut event_payload = NamedTempFile::new_in(tmp.path()) .expect("Failed to spawn a tmp file for test event payload"); unsafe { + env::set_var("GITHUB_ACTIONS", "true"); env::set_var("GITHUB_REPOSITORY", REPO); env::set_var("GITHUB_SHA", SHA); env::set_var("GITHUB_TOKEN", TOKEN); @@ -66,8 +63,17 @@ async fn get_paginated_changes(lib_root: &Path, test_params: &TestParams) { && !test_params.fail_serde_event_payload && !test_params.no_event_payload { + let payload = serde_json::json!({ + "pull_request": { + "draft": false, + "state": "open", + "number": PR, + "locked": false, + } + }) + .to_string(); event_payload - .write_all(EVENT_PAYLOAD.as_bytes()) + .write_all(payload.as_bytes()) .expect("Failed to write data to test event payload file") } @@ -81,7 +87,7 @@ async fn get_paginated_changes(lib_root: &Path, test_params: &TestParams) { env::set_current_dir(tmp.path()).unwrap(); logger::try_init(); log::set_max_level(log::LevelFilter::Debug); - let gh_client = GithubApiClient::new(); + let gh_client = RestClient::new(); if test_params.fail_serde_event_payload || test_params.no_event_payload { assert!(gh_client.is_err()); return; @@ -97,16 +103,6 @@ async fn get_paginated_changes(lib_root: &Path, test_params: &TestParams) { format!("commits/{SHA}") } ); - mocks.push( - server - .mock("GET", diff_end_point.as_str()) - .match_header("Accept", "application/vnd.github.diff") - .match_header("Authorization", format!("token {TOKEN}").as_str()) - .with_header(REMAINING_RATE_LIMIT_HEADER, "50") - .with_header(RESET_RATE_LIMIT_HEADER, reset_timestamp.as_str()) - .with_status(403) - .create(), - ); let pg_end_point = if test_params.event_t == EventType::Push { diff_end_point.clone() } else { @@ -142,9 +138,9 @@ async fn get_paginated_changes(lib_root: &Path, test_params: &TestParams) { mocks.push(mock.create()); } - let file_filter = FileFilter::new(&[], vec!["cpp".to_string(), "hpp".to_string()]); + let file_filter = FileFilter::new(&[], &["cpp", "hpp"], None); let files = client - .get_list_of_changed_files(&file_filter, &LinesChangedOnly::Off, &None::, false) + .get_list_of_changed_files(&file_filter, &LinesChangedOnly::Off, &None::, false) .await; match files { Err(e) => { diff --git a/cpp-linter/tests/reviews.rs b/cpp-linter/tests/reviews.rs index 21fa2308..f18c7557 100644 --- a/cpp-linter/tests/reviews.rs +++ b/cpp-linter/tests/reviews.rs @@ -1,11 +1,11 @@ +#![cfg(feature = "bin")] use chrono::Utc; use cpp_linter::{ - cli::LinesChangedOnly, - rest_api::{COMMENT_MARKER, USER_OUTREACH}, + rest_client::{COMMENT_MARKER, USER_OUTREACH}, run::run_main, }; +use git_bot_feedback::LinesChangedOnly; use mockito::Matcher; -use serde_json::json; use std::{env, io::Write, path::Path}; use tempfile::NamedTempFile; @@ -17,7 +17,6 @@ const REPO: &str = "cpp-linter/test-cpp-linter-action"; const PR: i64 = 27; const TOKEN: &str = "123456"; const MOCK_ASSETS_PATH: &str = "tests/reviews_test_assets/"; -const EVENT_PAYLOAD: &str = "{\"number\": 27}"; const RESET_RATE_LIMIT_HEADER: &str = "x-ratelimit-reset"; const REMAINING_RATE_LIMIT_HEADER: &str = "x-ratelimit-remaining"; @@ -73,11 +72,22 @@ fn generate_tool_summary(review_enabled: bool, force_lgtm: bool, tool_name: &str async fn setup(lib_root: &Path, test_params: &TestParams) { let mut event_payload_path = NamedTempFile::new_in("./").unwrap(); + let event_payload = serde_json::json!({ + "pull_request": { + "draft": test_params.pr_draft, + "state": test_params.pr_state, + "number": PR, + "locked": false, + } + }) + .to_string(); event_payload_path - .write_all(EVENT_PAYLOAD.as_bytes()) + .write_all(event_payload.as_bytes()) .expect("Failed to create mock event payload."); + let tmp_out = NamedTempFile::new().unwrap(); unsafe { - env::remove_var("GITHUB_OUTPUT"); // avoid writing to GH_OUT in parallel-running tests + env::set_var("GITHUB_ACTIONS", "true"); + env::set_var("GITHUB_OUTPUT", tmp_out.path()); env::set_var("GITHUB_EVENT_NAME", "pull_request"); env::set_var("GITHUB_REPOSITORY", REPO); env::set_var("GITHUB_SHA", SHA); @@ -101,24 +111,11 @@ async fn setup(lib_root: &Path, test_params: &TestParams) { let pr_endpoint = format!("/repos/{REPO}/pulls/{PR}"); mocks.push( server - .mock("GET", pr_endpoint.as_str()) - .match_header("Accept", "application/vnd.github.diff") - .match_header("Authorization", format!("token {TOKEN}").as_str()) - .with_body_from_file(format!("{asset_path}pr_{PR}.diff")) - .with_header(REMAINING_RATE_LIMIT_HEADER, "50") - .with_header(RESET_RATE_LIMIT_HEADER, reset_timestamp.as_str()) - .create(), - ); - mocks.push( - server - .mock("GET", pr_endpoint.as_str()) + .mock("GET", format!("{pr_endpoint}/files").as_str()) .match_header("Accept", "application/vnd.github.raw+json") .match_header("Authorization", format!("token {TOKEN}").as_str()) - .with_body(if test_params.bad_pr_info { - String::new() - } else { - json!({"state": test_params.pr_state, "draft": test_params.pr_draft}).to_string() - }) + .match_query(Matcher::Any) + .with_body_from_file(format!("{asset_path}pr_27.json")) .with_header(REMAINING_RATE_LIMIT_HEADER, "50") .with_header(RESET_RATE_LIMIT_HEADER, reset_timestamp.as_str()) .create(), @@ -126,35 +123,40 @@ async fn setup(lib_root: &Path, test_params: &TestParams) { let reviews_endpoint = format!("/repos/{REPO}/pulls/{PR}/reviews"); - let mut mock = server - .mock("GET", reviews_endpoint.as_str()) - .match_header("Accept", "application/vnd.github.raw+json") - .match_header("Authorization", format!("token {TOKEN}").as_str()) - .match_body(Matcher::Any) - .match_query(Matcher::UrlEncoded("page".to_string(), "1".to_string())) - .with_header(REMAINING_RATE_LIMIT_HEADER, "50") - .with_header(RESET_RATE_LIMIT_HEADER, reset_timestamp.as_str()) - .with_status(if test_params.fail_get_existing_reviews { - 403 + if test_params.pr_state != "closed" { + let mut mock = server + .mock("GET", reviews_endpoint.as_str()) + .match_header("Accept", "application/vnd.github.raw+json") + .match_header("Authorization", format!("token {TOKEN}").as_str()) + .match_body(Matcher::Any) + .match_query(Matcher::UrlEncoded("page".to_string(), "1".to_string())) + .with_header(REMAINING_RATE_LIMIT_HEADER, "50") + .with_header(RESET_RATE_LIMIT_HEADER, reset_timestamp.as_str()) + .with_status(if test_params.fail_get_existing_reviews { + 403 + } else { + 200 + }); + if test_params.bad_existing_reviews { + mock = mock.with_body(String::new()).create(); } else { - 200 - }); - if test_params.bad_existing_reviews { - mock = mock.with_body(String::new()).create(); - } else { - mock = mock - .with_body_from_file(format!("{asset_path}pr_reviews.json")) - .create() + mock = mock + .with_body_from_file(format!("{asset_path}pr_reviews.json")) + .create() + } + mocks.push(mock); } - mocks.push(mock); - if !test_params.fail_get_existing_reviews && !test_params.bad_existing_reviews { + if !test_params.fail_get_existing_reviews + && !test_params.bad_existing_reviews + && test_params.pr_state != "closed" + { mocks.push( server .mock( "PUT", format!("{reviews_endpoint}/1807607546/dismissals").as_str(), ) - .match_body(r#"{"event":"DISMISS","message":"outdated suggestion"}"#) + .match_body(r#"{"event":"DISMISS","message":"outdated review"}"#) .match_header("Authorization", format!("token {TOKEN}").as_str()) .with_header(REMAINING_RATE_LIMIT_HEADER, "50") .with_header(RESET_RATE_LIMIT_HEADER, reset_timestamp.as_str()) @@ -243,8 +245,18 @@ async fn setup(lib_root: &Path, test_params: &TestParams) { } else { args.push("--style=file".to_string()); // use .clang-format file } - let result = run_main(args).await; - assert!(result.is_ok()); + match run_main(args).await { + Ok(_) => { + if test_params.bad_existing_reviews { + panic!("Expected failure, but it succeeded"); + } + } + Err(e) => { + if !test_params.bad_existing_reviews { + panic!("Failed unexpectedly: {e:?}"); + } + } + } for mock in mocks { mock.assert(); } diff --git a/cpp-linter/tests/reviews_test_assets/pr_27.diff b/cpp-linter/tests/reviews_test_assets/pr_27.diff deleted file mode 100644 index 3c5dd0b5..00000000 --- a/cpp-linter/tests/reviews_test_assets/pr_27.diff +++ /dev/null @@ -1,108 +0,0 @@ -diff --git a/.github/workflows/cpp-lint-package.yml b/.github/workflows/cpp-lint-package.yml -index 0418957..3b8c454 100644 ---- a/.github/workflows/cpp-lint-package.yml -+++ b/.github/workflows/cpp-lint-package.yml -@@ -7,6 +7,7 @@ on: - description: 'which branch to test' - default: 'main' - required: true -+ pull_request: - - jobs: - cpp-linter: -@@ -14,9 +15,9 @@ jobs: - - strategy: - matrix: -- clang-version: ['7', '8', '9','10', '11', '12', '13', '14', '15', '16', '17'] -+ clang-version: ['10', '11', '12', '13', '14', '15', '16', '17'] - repo: ['cpp-linter/cpp-linter'] -- branch: ['${{ inputs.branch }}'] -+ branch: ['pr-review-suggestions'] - fail-fast: false - - steps: -@@ -62,10 +63,12 @@ jobs: - -i=build - -p=build - -V=${{ runner.temp }}/llvm -- -f=false - --extra-arg="-std=c++14 -Wall" -- --thread-comments=${{ matrix.clang-version == '12' }} -- -a=${{ matrix.clang-version == '12' }} -+ --file-annotations=false -+ --lines-changed-only=true -+ --thread-comments=${{ matrix.clang-version == '16' }} -+ --tidy-review=${{ matrix.clang-version == '16' }} -+ --format-review=${{ matrix.clang-version == '16' }} - - - name: Fail fast?! - if: steps.linter.outputs.checks-failed > 0 -diff --git a/src/demo.cpp b/src/demo.cpp -index 0c1db60..1bf553e 100644 ---- a/src/demo.cpp -+++ b/src/demo.cpp -@@ -1,17 +1,18 @@ - /** This is a very ugly test code (doomed to fail linting) */ - #include "demo.hpp" --#include --#include -+#include - --// using size_t from cstddef --size_t dummyFunc(size_t i) { return i; } - --int main() --{ -- for (;;) -- break; -+ -+ -+int main(){ -+ -+ for (;;) break; -+ - - printf("Hello world!\n"); - -- return 0; --} -+ -+ -+ -+ return 0;} -diff --git a/src/demo.hpp b/src/demo.hpp -index 2695731..f93d012 100644 ---- a/src/demo.hpp -+++ b/src/demo.hpp -@@ -5,12 +5,10 @@ - class Dummy { - char* useless; - int numb; -+ Dummy() :numb(0), useless("\0"){} - - public: -- void *not_usefull(char *str){ -- useless = str; -- return 0; -- } -+ void *not_useful(char *str){useless = str;} - }; - - -@@ -28,14 +26,11 @@ class Dummy { - - - -- -- -- -- - - - struct LongDiff - { -+ - long diff; - - }; diff --git a/cpp-linter/tests/reviews_test_assets/pr_27.json b/cpp-linter/tests/reviews_test_assets/pr_27.json new file mode 100644 index 00000000..f28a8901 --- /dev/null +++ b/cpp-linter/tests/reviews_test_assets/pr_27.json @@ -0,0 +1,38 @@ +[ + { + "sha": "52501fa1dc96d6bc6f8a155816df041b1de975d9", + "filename": ".github/workflows/cpp-lint-package.yml", + "status": "modified", + "additions": 9, + "deletions": 5, + "changes": 14, + "blob_url": "https://github.com/cpp-linter/test-cpp-linter-action/blob/635a9c57bdcca07b99ddef52c2640337c50280b1/.github%2Fworkflows%2Fcpp-lint-package.yml", + "raw_url": "https://github.com/cpp-linter/test-cpp-linter-action/raw/635a9c57bdcca07b99ddef52c2640337c50280b1/.github%2Fworkflows%2Fcpp-lint-package.yml", + "contents_url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/contents/.github%2Fworkflows%2Fcpp-lint-package.yml?ref=635a9c57bdcca07b99ddef52c2640337c50280b1", + "patch": "@@ -7,16 +7,17 @@ on:\n description: 'which branch to test'\n default: 'main'\n required: true\n+ pull_request:\n \n jobs:\n cpp-linter:\n runs-on: windows-latest\n \n strategy:\n matrix:\n- clang-version: ['7', '8', '9','10', '11', '12', '13', '14', '15', '16', '17']\n+ clang-version: ['10', '11', '12', '13', '14', '15', '16', '17']\n repo: ['cpp-linter/cpp-linter']\n- branch: ['${{ inputs.branch }}']\n+ branch: ['pr-review-suggestions']\n fail-fast: false\n \n steps:\n@@ -62,10 +63,13 @@ jobs:\n -i=build \n -p=build \n -V=${{ runner.temp }}/llvm \n- -f=false \n --extra-arg=\"-std=c++14 -Wall\" \n- --thread-comments=${{ matrix.clang-version == '12' }} \n- -a=${{ matrix.clang-version == '12' }}\n+ --file-annotations=false\n+ --lines-changed-only=false\n+ --extension=h,c\n+ --thread-comments=${{ matrix.clang-version == '16' }} \n+ --tidy-review=${{ matrix.clang-version == '16' }}\n+ --format-review=${{ matrix.clang-version == '16' }}\n \n - name: Fail fast?!\n if: steps.linter.outputs.checks-failed > 0" + }, + { + "sha": "1bf553e06e4b7c6c9a9be5da4845acbdeb04f6a5", + "filename": "src/demo.cpp", + "status": "modified", + "additions": 11, + "deletions": 10, + "changes": 21, + "blob_url": "https://github.com/cpp-linter/test-cpp-linter-action/blob/635a9c57bdcca07b99ddef52c2640337c50280b1/src%2Fdemo.cpp", + "raw_url": "https://github.com/cpp-linter/test-cpp-linter-action/raw/635a9c57bdcca07b99ddef52c2640337c50280b1/src%2Fdemo.cpp", + "contents_url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/contents/src%2Fdemo.cpp?ref=635a9c57bdcca07b99ddef52c2640337c50280b1", + "patch": "@@ -1,17 +1,18 @@\n /** This is a very ugly test code (doomed to fail linting) */\n #include \"demo.hpp\"\n-#include \n-#include \n+#include \n \n-// using size_t from cstddef\n-size_t dummyFunc(size_t i) { return i; }\n \n-int main()\n-{\n- for (;;)\n- break;\n+\n+\n+int main(){\n+\n+ for (;;) break;\n+\n \n printf(\"Hello world!\\n\");\n \n- return 0;\n-}\n+\n+\n+\n+ return 0;}" + }, + { + "sha": "f93d0122ae2e3c1952c795837d71c432036b55eb", + "filename": "src/demo.hpp", + "status": "modified", + "additions": 3, + "deletions": 8, + "changes": 11, + "blob_url": "https://github.com/cpp-linter/test-cpp-linter-action/blob/635a9c57bdcca07b99ddef52c2640337c50280b1/src%2Fdemo.hpp", + "raw_url": "https://github.com/cpp-linter/test-cpp-linter-action/raw/635a9c57bdcca07b99ddef52c2640337c50280b1/src%2Fdemo.hpp", + "contents_url": "https://api.github.com/repos/cpp-linter/test-cpp-linter-action/contents/src%2Fdemo.hpp?ref=635a9c57bdcca07b99ddef52c2640337c50280b1", + "patch": "@@ -5,12 +5,10 @@\n class Dummy {\n char* useless;\n int numb;\n+ Dummy() :numb(0), useless(\"\\0\"){}\n \n public:\n- void *not_usefull(char *str){\n- useless = str;\n- return 0;\n- }\n+ void *not_useful(char *str){useless = str;}\n };\n \n \n@@ -28,14 +26,11 @@ class Dummy {\n \n \n \n-\n-\n-\n-\n \n \n struct LongDiff\n {\n+\n long diff;\n \n };" + } +] From 89d1c267190b810e49517f4155aa7c7905045c0f Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Tue, 28 Apr 2026 01:34:17 -0700 Subject: [PATCH 02/18] bump cargo deps, select TLS backend, update tests --- Cargo.lock | 736 ++++++++----------------------------- clang-installer/Cargo.toml | 2 +- cpp-linter/Cargo.toml | 3 +- cpp-linter/src/git.rs | 51 ++- 4 files changed, 194 insertions(+), 598 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7b3397d9..57ed4d4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,9 +43,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -121,28 +121,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "aws-lc-rs" -version = "1.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "base64" version = "0.22.1" @@ -151,9 +129,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "blake2" @@ -187,9 +165,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -197,24 +175,12 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - [[package]] name = "chrono" version = "0.4.44" @@ -246,7 +212,7 @@ dependencies = [ "serde_json", "sha2", "tempfile", - "thiserror 2.0.18", + "thiserror", "tokio", "url", "which", @@ -289,9 +255,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cli-gen" @@ -302,20 +268,11 @@ dependencies = [ "pyo3", ] -[[package]] -name = "cmake" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" -dependencies = [ - "cc", -] - [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" @@ -326,16 +283,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - [[package]] name = "convert_case" version = "0.11.0" @@ -394,7 +341,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror 2.0.18", + "thiserror", "tokio", ] @@ -448,9 +395,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.8.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +checksum = "83cf0d42651b16c6dfe68685716d18480d18a9c39c62d76e8cf3eb6ed5d8bcbf" dependencies = [ "ctor-proc-macro", "dtor", @@ -458,9 +405,9 @@ dependencies = [ [[package]] name = "ctor-proc-macro" -version = "0.0.7" +version = "0.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" +checksum = "7a949c44fcacbbbb7ada007dc7acb34603dd97cd47de5d054f2b6493ecebb483" [[package]] name = "digest" @@ -507,24 +454,18 @@ dependencies = [ [[package]] name = "dtor" -version = "0.3.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b" dependencies = [ "dtor-proc-macro", ] [[package]] name = "dtor-proc-macro" -version = "0.0.6" +version = "0.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +checksum = "2647271c92754afcb174e758003cfd1cbf1e43e5a7853d7b1813e63e19e39a73" [[package]] name = "encoding_rs" @@ -562,9 +503,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "find-msvc-tools" @@ -618,12 +559,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "futures" version = "0.3.32" @@ -729,10 +664,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] [[package]] @@ -742,11 +675,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] @@ -764,9 +695,9 @@ dependencies = [ [[package]] name = "git-bot-feedback" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dde52ca9dc5b3d9227c79b910a804036f0599a563767015683d9b3fa6842874" +checksum = "b6bf43abc02f0a7c68ab167e3a9b11dbeb7032dec48d5d9097ce19d77a6892b8" dependencies = [ "async-trait", "chrono", @@ -776,7 +707,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "url", ] @@ -826,9 +757,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.1" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -883,9 +814,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -898,7 +829,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -906,15 +836,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -987,12 +916,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1000,9 +930,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1013,9 +943,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1027,15 +957,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1047,15 +977,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1085,9 +1015,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1095,12 +1025,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -1113,9 +1043,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.10" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" dependencies = [ "memchr", "serde", @@ -1129,31 +1059,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -1167,10 +1075,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -1183,9 +1093,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libgit2-sys" @@ -1213,9 +1123,9 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ "libc", ] @@ -1236,9 +1146,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.24" +version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4735e9cbde5aac84a5ce588f6b23a90b9b0b528f6c5a8db8a4aff300463a0839" +checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" dependencies = [ "cc", "libc", @@ -1254,9 +1164,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -1273,12 +1183,6 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "memchr" version = "2.8.0" @@ -1339,9 +1243,9 @@ dependencies = [ [[package]] name = "napi" -version = "3.8.4" +version = "3.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb7848c221fb7bb789e02f01875287ebb1e078b92a6566a34de01ef8806e7c2b" +checksum = "fa73b028610e2b26e9e40bd2c8ff8a98e6d7ed5d67d89ebf4bfd2f992616b024" dependencies = [ "bitflags", "ctor", @@ -1361,9 +1265,9 @@ checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1" [[package]] name = "napi-derive" -version = "3.5.3" +version = "3.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60867ff9a6f76e82350e0c3420cb0736f5866091b61d7d8a024baa54b0ec17dd" +checksum = "7430702d3cc05cf55f0a2c9e41d991c3b7a53f91e6146a8f282b1bfc7f3fd133" dependencies = [ "convert_case", "ctor", @@ -1375,9 +1279,9 @@ dependencies = [ [[package]] name = "napi-derive-backend" -version = "5.0.2" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0864cf6a82e2cfb69067374b64c9253d7e910e5b34db833ed7495dda56ccb18" +checksum = "1ca5a083f2c9b49a0c7d33ec75c083498849c6fcc46f5497317faa39ea77f5d5" dependencies = [ "convert_case", "proc-macro2", @@ -1429,9 +1333,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -1539,17 +1443,11 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "portable-atomic" @@ -1559,9 +1457,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -1662,62 +1560,6 @@ dependencies = [ "serde", ] -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - [[package]] name = "quote" version = "1.0.45" @@ -1785,7 +1627,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -1819,9 +1661,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64", "bytes", @@ -1841,14 +1683,10 @@ dependencies = [ "native-tls", "percent-encoding", "pin-project-lite", - "quinn", - "rustls", "rustls-pki-types", - "rustls-platform-verifier", "sync_wrapper", "tokio", "tokio-native-tls", - "tokio-rustls", "tower", "tower-http", "tower-service", @@ -1874,9 +1712,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustix" @@ -1893,11 +1731,10 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" dependencies = [ - "aws-lc-rs", "once_cell", "rustls-pki-types", "rustls-webpki", @@ -1905,62 +1742,21 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe 0.2.1", - "rustls-pki-types", - "schannel", - "security-framework", -] - [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ - "web-time", "zeroize", ] -[[package]] -name = "rustls-platform-verifier" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -1978,20 +1774,11 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -2105,9 +1892,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "similar" @@ -2226,33 +2013,13 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -2268,34 +2035,19 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" -version = "1.52.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -2429,9 +2181,9 @@ checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-ident" @@ -2441,9 +2193,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-xid" @@ -2493,16 +2245,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "want" version = "0.3.1" @@ -2520,11 +2262,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -2533,14 +2275,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -2551,23 +2293,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2575,9 +2313,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -2588,9 +2326,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -2631,33 +2369,14 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", ] -[[package]] -name = "webpki-root-certs" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "which" version = "8.0.2" @@ -2667,15 +2386,6 @@ dependencies = [ "libc", ] -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "windows-core" version = "0.62.2" @@ -2746,31 +2456,13 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -2782,192 +2474,70 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -2977,6 +2547,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" @@ -3058,15 +2634,15 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -3075,9 +2651,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -3087,18 +2663,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.41" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96e13bc581734df6250836c59a5f44f3c57db9f9acb9dc8e3eaabdaf6170254d" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.41" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3545ea9e86d12ab9bba9fcd99b54c1556fd3199007def5a03c375623d05fac1c" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -3107,18 +2683,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -3134,9 +2710,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -3145,9 +2721,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -3156,9 +2732,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -3167,9 +2743,9 @@ dependencies = [ [[package]] name = "zip" -version = "8.5.1" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcab981e19633ebcf0b001ddd37dd802996098bc1864f90b7c5d970ce76c1d59" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" dependencies = [ "crc32fast", "flate2", diff --git a/clang-installer/Cargo.toml b/clang-installer/Cargo.toml index e01f3fe7..fd1bc475 100644 --- a/clang-installer/Cargo.toml +++ b/clang-installer/Cargo.toml @@ -41,7 +41,7 @@ tempfile = { workspace = true } [dev-dependencies] colored = { workspace = true } mockito = { workspace = true } -reqwest = { workspace = true, default-features = true } +reqwest = { workspace = true, features = ["native-tls"] } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } [features] diff --git a/cpp-linter/Cargo.toml b/cpp-linter/Cargo.toml index c8363c82..c9ffc6a2 100644 --- a/cpp-linter/Cargo.toml +++ b/cpp-linter/Cargo.toml @@ -22,7 +22,7 @@ clap = { workspace = true, optional = true } colored = { workspace = true, optional = true } fast-glob = "1.0.1" futures = "0.3.32" -git-bot-feedback = { version = "0.5.2", features = ["file-changes"] } +git-bot-feedback = { version = "0.5.3", features = ["file-changes"] } git2 = "0.20.4" log = { workspace = true } quick-xml = { version = "0.39.2", features = ["serialize"] } @@ -38,6 +38,7 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } mockito = { workspace = true } tempfile = { workspace = true } git2 = { version = "0.20.4", features = ["https"]} +reqwest = { workspace = true, features = ["native-tls"] } [features] openssl-vendored = ["git2/vendored-openssl", "reqwest/native-tls-vendored"] diff --git a/cpp-linter/src/git.rs b/cpp-linter/src/git.rs index c93e1bf7..10055f2e 100644 --- a/cpp-linter/src/git.rs +++ b/cpp-linter/src/git.rs @@ -271,7 +271,7 @@ mod test { async fn checkout_cpp_linter_py_repo( sha: &str, - extensions: &[String], + extensions: &[&str], tmp: &TempDir, patch_path: Option<&str>, ignore_staged: bool, @@ -287,16 +287,12 @@ mod test { env::set_var("CI", "false"); } let rest_api_client = RestClient::new().unwrap(); - let file_filter = FileFilter::new( - &["target"], - &extensions.iter().map(|s| s.as_str()).collect::>(), - None, - ); + let file_filter = FileFilter::new(&["target"], extensions, None); set_current_dir(tmp).unwrap(); let base_diff = if ignore_staged { Some("0".to_string()) } else { - None:: + None }; rest_api_client .get_list_of_changed_files( @@ -315,7 +311,7 @@ mod test { let sha = "0c236809891000b16952576dc34de082d7a40bf3"; let cur_dir = current_dir().unwrap(); let tmp = get_temp_dir(); - let extensions = vec!["cpp".to_string(), "hpp".to_string()]; + let extensions = ["cpp", "hpp"]; let files = checkout_cpp_linter_py_repo(sha, &extensions, &tmp, None, false).await; println!("files = {:?}", files); assert!(files.is_empty()); @@ -329,13 +325,21 @@ mod test { let sha = "950ff0b690e1903797c303c5fc8d9f3b52f1d3c5"; let cur_dir = current_dir().unwrap(); let tmp = get_temp_dir(); - let extensions = vec!["cpp".to_string(), "hpp".to_string()]; - let files = checkout_cpp_linter_py_repo(sha, &extensions.clone(), &tmp, None, false).await; + let extensions = ["cpp", "hpp"]; + let files = checkout_cpp_linter_py_repo(sha, &extensions, &tmp, None, false).await; println!("files = {:?}", files); assert!(files.len() >= 2); for file in files { assert!( - extensions.contains(&file.name.extension().unwrap().to_string_lossy().to_string()) + extensions.contains( + &file + .name + .extension() + .unwrap() + .to_string_lossy() + .to_string() + .as_str() + ) ); } set_current_dir(cur_dir).unwrap(); // prep to delete temp_folder @@ -348,10 +352,10 @@ mod test { let sha = "0c236809891000b16952576dc34de082d7a40bf3"; let cur_dir = current_dir().unwrap(); let tmp = get_temp_dir(); - let extensions = vec!["cpp".to_string(), "hpp".to_string()]; + let extensions = ["cpp", "hpp"]; let files = checkout_cpp_linter_py_repo( sha, - &extensions.clone(), + &extensions, &tmp, Some("tests/git_status_test_assets/cpp-linter/cpp-linter/test_git_lib.patch"), false, @@ -361,7 +365,15 @@ mod test { assert!(!files.is_empty()); for file in files { assert!( - extensions.contains(&file.name.extension().unwrap().to_string_lossy().to_string()) + extensions.contains( + &file + .name + .extension() + .unwrap() + .to_string_lossy() + .to_string() + .as_str() + ) ); } set_current_dir(cur_dir).unwrap(); // prep to delete temp_folder @@ -374,16 +386,23 @@ mod test { let sha = "0c236809891000b16952576dc34de082d7a40bf3"; let cur_dir = current_dir().unwrap(); let tmp = get_temp_dir(); - let extensions = vec!["cpp".to_string(), "hpp".to_string()]; + let extensions = ["cpp", "hpp"]; let files = checkout_cpp_linter_py_repo( sha, - &extensions.clone(), + &extensions, &tmp, Some("tests/git_status_test_assets/cpp-linter/cpp-linter/test_git_lib.patch"), true, ) .await; println!("files = {:?}", files); + let git_status = std::process::Command::new("git") + .args(["diff", "HEAD~1..HEAD"]) + .output() + .map(|o| String::from_utf8(o.stdout).unwrap()) + .unwrap(); + eprintln!("git status:\n{git_status}"); + eprintln!("files: {files:?}"); assert!(files.is_empty()); set_current_dir(cur_dir).unwrap(); // prep to delete temp_folder drop(tmp); // delete temp_folder From 02433e56198615ee94c6f23bfae618c32eda9251 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Tue, 2 Jun 2026 23:41:34 -0700 Subject: [PATCH 03/18] bump rust deps --- Cargo.lock | 222 ++++++++++++++----------------------- Cargo.toml | 10 +- bindings/node/Cargo.toml | 6 +- clang-installer/Cargo.toml | 2 +- cpp-linter/Cargo.toml | 6 +- 5 files changed, 98 insertions(+), 148 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 57ed4d4a..c65962ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,9 +117,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base64" @@ -129,9 +129,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" [[package]] name = "blake2" @@ -153,9 +153,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytes" @@ -165,9 +165,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.61" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ "find-msvc-tools", "jobserver", @@ -395,19 +395,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83cf0d42651b16c6dfe68685716d18480d18a9c39c62d76e8cf3eb6ed5d8bcbf" -dependencies = [ - "ctor-proc-macro", - "dtor", -] - -[[package]] -name = "ctor-proc-macro" -version = "0.0.13" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a949c44fcacbbbb7ada007dc7acb34603dd97cd47de5d054f2b6493ecebb483" +checksum = "01334b89b69ff726750c5ce5073fc8bd860e99aa9a8fc5ca11b04730e3aee97a" [[package]] name = "digest" @@ -443,30 +433,15 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "dtor" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b" -dependencies = [ - "dtor-proc-macro", -] - -[[package]] -name = "dtor-proc-macro" -version = "0.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2647271c92754afcb174e758003cfd1cbf1e43e5a7853d7b1813e63e19e39a73" - [[package]] name = "encoding_rs" version = "0.8.35" @@ -714,9 +689,9 @@ dependencies = [ [[package]] name = "git2" -version = "0.20.4" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" +checksum = "ddddbf932745a6be37109b6112d3ee09696106f848449069d3a57bba937ab82e" dependencies = [ "bitflags", "libc", @@ -729,9 +704,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", @@ -757,9 +732,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -769,9 +744,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -814,9 +789,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -1030,7 +1005,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -1041,16 +1016,6 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "iri-string" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1075,9 +1040,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if", "futures-util", @@ -1099,13 +1064,12 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libgit2-sys" -version = "0.18.3+1.9.2" +version = "0.18.5+1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" +checksum = "005d6ae6eac1912906073e069f7db60b1fa98e052a68227824afe3e3a1c59ca2" dependencies = [ "cc", "libc", - "libssh2-sys", "libz-sys", "openssl-sys", "pkg-config", @@ -1123,32 +1087,18 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ "libc", ] -[[package]] -name = "libssh2-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" -dependencies = [ - "cc", - "libc", - "libz-sys", - "openssl-sys", - "pkg-config", - "vcpkg", -] - [[package]] name = "libz-sys" -version = "1.1.28" +version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" dependencies = [ "cc", "libc", @@ -1179,15 +1129,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "mime" @@ -1207,9 +1157,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -1243,9 +1193,9 @@ dependencies = [ [[package]] name = "napi" -version = "3.8.5" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa73b028610e2b26e9e40bd2c8ff8a98e6d7ed5d67d89ebf4bfd2f992616b024" +checksum = "f1d395473824516f38dd1071a1a37bc57daa7be65b293ebba4ead5f7abb017a2" dependencies = [ "bitflags", "ctor", @@ -1259,15 +1209,15 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d376940fd5b723c6893cd1ee3f33abbfd86acb1cd1ec079f3ab04a2a3bc4d3b1" +checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1" [[package]] name = "napi-derive" -version = "3.5.4" +version = "3.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7430702d3cc05cf55f0a2c9e41d991c3b7a53f91e6146a8f282b1bfc7f3fd133" +checksum = "89b3f766e04667e6da0e181e2da4f85475d5a6513b7cf6a80bea184e224a5b42" dependencies = [ "convert_case", "ctor", @@ -1279,9 +1229,9 @@ dependencies = [ [[package]] name = "napi-derive-backend" -version = "5.0.3" +version = "5.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca5a083f2c9b49a0c7d33ec75c083498849c6fcc46f5497317faa39ea77f5d5" +checksum = "0d5af30503edf933ce7377cf6d4c877a62b0f1107ea05585f1b5e430e88d5baf" dependencies = [ "convert_case", "proc-macro2", @@ -1382,9 +1332,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-src" -version = "300.5.5+3.5.5" +version = "300.6.0+3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" +checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" dependencies = [ "cc", ] @@ -1552,9 +1502,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.39.2" +version = "0.40.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +checksum = "2474bd2e5029e7ccb6abb2ba48cf2383a333851dedf495901544281590c7da7f" dependencies = [ "memchr", "serde", @@ -1661,9 +1611,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -1731,9 +1681,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.39" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "once_cell", "rustls-pki-types", @@ -1850,9 +1800,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -1886,9 +1836,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "simd-adler32" @@ -1916,9 +1866,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -2045,9 +1995,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -2120,20 +2070,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -2181,9 +2131,9 @@ checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "unicode-ident" @@ -2193,9 +2143,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-xid" @@ -2280,9 +2230,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -2293,9 +2243,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.68" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ "js-sys", "wasm-bindgen", @@ -2303,9 +2253,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2313,9 +2263,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -2326,9 +2276,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -2369,9 +2319,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.95" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -2663,18 +2613,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" dependencies = [ "proc-macro2", "quote", @@ -2683,9 +2633,9 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] diff --git a/Cargo.toml b/Cargo.toml index c5de9c41..2e63d206 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,16 +21,16 @@ strip = "symbols" [workspace.dependencies] anyhow = "1.0.102" -clap = { version = "4.6.0", features = ["derive"] } +clap = { version = "4.6.1", features = ["derive"] } colored = "3.1.1" -log = { version = "0.4.29", features = ["std"] } +log = { version = "0.4.31", features = ["std"] } mockito = "1.7.2" pyo3 = {version = "0.28.3", features = ["extension-module"] } regex = "1.12.3" -reqwest = { version = "0.13.2", default-features = false, features = ["http2", "charset"] } +reqwest = { version = "0.13.4", default-features = false, features = ["http2", "charset"] } thiserror = "2.0.18" semver = "1.0.28" serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0.149" +serde_json = "1.0.150" tempfile = "3.27.0" -tokio = "1.51.1" +tokio = "1.52.3" diff --git a/bindings/node/Cargo.toml b/bindings/node/Cargo.toml index 71ff9edc..7ae566a8 100644 --- a/bindings/node/Cargo.toml +++ b/bindings/node/Cargo.toml @@ -17,8 +17,8 @@ bench = false [dependencies] # Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix -napi = { version = "3.8.4", default-features = false, features = ["napi4", "async"] } -napi-derive = "3.5.3" +napi = { version = "3.9.0", default-features = false, features = ["napi4", "async"] } +napi-derive = "3.5.6" cpp-linter = { path = "../../cpp-linter", features = ["bin"] } anyhow = { workspace = true } @@ -26,4 +26,4 @@ anyhow = { workspace = true } openssl-vendored = ["cpp-linter/openssl-vendored"] [build-dependencies] -napi-build = "2.3.1" +napi-build = "2.3.2" diff --git a/clang-installer/Cargo.toml b/clang-installer/Cargo.toml index fd1bc475..9a692a98 100644 --- a/clang-installer/Cargo.toml +++ b/clang-installer/Cargo.toml @@ -35,7 +35,7 @@ thiserror = { workspace = true } tokio = { workspace = true, features = ["macros"], optional = true } url = "2.5.8" which = "8.0.2" -zip = { version = "8.5.1", default-features = false, features = ["deflate"] } +zip = { version = "8.6.0", default-features = false, features = ["deflate"] } tempfile = { workspace = true } [dev-dependencies] diff --git a/cpp-linter/Cargo.toml b/cpp-linter/Cargo.toml index c9ffc6a2..12e4daf8 100644 --- a/cpp-linter/Cargo.toml +++ b/cpp-linter/Cargo.toml @@ -23,9 +23,9 @@ colored = { workspace = true, optional = true } fast-glob = "1.0.1" futures = "0.3.32" git-bot-feedback = { version = "0.5.3", features = ["file-changes"] } -git2 = "0.20.4" +git2 = { version = "0.21.0", features = ["https"] } log = { workspace = true } -quick-xml = { version = "0.39.2", features = ["serialize"] } +quick-xml = { version = "0.40.1", features = ["serialize"] } regex = { workspace = true } reqwest = { workspace = true } semver = { workspace = true } @@ -37,7 +37,7 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } [dev-dependencies] mockito = { workspace = true } tempfile = { workspace = true } -git2 = { version = "0.20.4", features = ["https"]} +git2 = { version = "0.21.0", features = ["https"] } reqwest = { workspace = true, features = ["native-tls"] } [features] From c8c9ffdfc09f093ea56a367e400ccfff81afc11d Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Wed, 3 Jun 2026 03:37:20 -0700 Subject: [PATCH 04/18] stub a GITHUB_OUTPUT env var with tmp path --- cpp-linter/src/run.rs | 45 +++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/cpp-linter/src/run.rs b/cpp-linter/src/run.rs index 0726d48d..65ebcc9a 100644 --- a/cpp-linter/src/run.rs +++ b/cpp-linter/src/run.rs @@ -190,11 +190,21 @@ mod test { use super::run_main; use std::env; - #[tokio::test] - async fn normal() { + /// helper to avoid writing to the same GITHUB_OUTPUT file in parallel-running tests. + fn setup_tmp_gh_out_path() -> tempfile::NamedTempFile { + let gh_out_path = tempfile::NamedTempFile::new().unwrap(); unsafe { - env::remove_var("GITHUB_OUTPUT"); // avoid writing to GH_OUT in parallel-running tests + env::set_var( + "GITHUB_OUTPUT", + gh_out_path.path().to_string_lossy().to_string(), + ); } + gh_out_path + } + + #[tokio::test] + async fn normal() { + let tmp_gh_out = setup_tmp_gh_out_path(); run_main(vec![ "cpp-linter".to_string(), "-l".to_string(), @@ -205,23 +215,21 @@ mod test { ]) .await .unwrap(); + drop(tmp_gh_out); } #[tokio::test] async fn version_command() { - unsafe { - env::remove_var("GITHUB_OUTPUT"); // avoid writing to GH_OUT in parallel-running tests - } + let tmp_gh_out = setup_tmp_gh_out_path(); run_main(vec!["cpp-linter".to_string(), "version".to_string()]) .await .unwrap(); + drop(tmp_gh_out); } #[tokio::test] async fn force_debug_output() { - unsafe { - env::remove_var("GITHUB_OUTPUT"); // avoid writing to GH_OUT in parallel-running tests - } + let tmp_gh_out = setup_tmp_gh_out_path(); run_main(vec![ "cpp-linter".to_string(), "-l".to_string(), @@ -231,13 +239,12 @@ mod test { ]) .await .unwrap(); + drop(tmp_gh_out); } #[tokio::test] async fn no_version_input() { - unsafe { - env::remove_var("GITHUB_OUTPUT"); // avoid writing to GH_OUT in parallel-running tests - } + let tmp_gh_out = setup_tmp_gh_out_path(); run_main(vec![ "cpp-linter".to_string(), "-l".to_string(), @@ -246,12 +253,13 @@ mod test { ]) .await .unwrap(); + drop(tmp_gh_out); } #[tokio::test] async fn pre_commit_env() { + let tmp_gh_out = setup_tmp_gh_out_path(); unsafe { - env::remove_var("GITHUB_OUTPUT"); // avoid writing to GH_OUT in parallel-running tests env::set_var("PRE_COMMIT", "1"); } run_main(vec![ @@ -262,15 +270,14 @@ mod test { ]) .await .unwrap_err(); + drop(tmp_gh_out); } // Verifies that the system gracefully handles cases where all analysis is disabled. // This ensures no diagnostic comments are generated when analysis is explicitly skipped. #[tokio::test] async fn no_analysis() { - unsafe { - env::remove_var("GITHUB_OUTPUT"); // avoid writing to GH_OUT in parallel-running tests - } + let tmp_gh_out = setup_tmp_gh_out_path(); run_main(vec![ "cpp-linter".to_string(), "-l".to_string(), @@ -281,13 +288,12 @@ mod test { ]) .await .unwrap(); + drop(tmp_gh_out); } #[tokio::test] async fn bad_repo_root() { - unsafe { - env::remove_var("GITHUB_OUTPUT"); // avoid writing to GH_OUT in parallel-running tests - } + let tmp_gh_out = setup_tmp_gh_out_path(); run_main(vec![ "cpp-linter".to_string(), "--repo-root".to_string(), @@ -295,5 +301,6 @@ mod test { ]) .await .unwrap_err(); + drop(tmp_gh_out); } } From 6cd46d01d11275ffd4f747a1f6f8348ab52b48c9 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Wed, 3 Jun 2026 03:38:37 -0700 Subject: [PATCH 05/18] reduce test job's output in CI logs We don't need to see all logs for passing tests --- .config/nextest.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.config/nextest.toml b/.config/nextest.toml index 6448b043..d7ba456e 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -27,9 +27,8 @@ default-filter = 'test(#*eval_version)' # This is all tests in tests/ folder + unit test for --extra-args. default-filter = "kind(test) + test(#*use_extra_args) + test(#*eval_version)" -# show log output from each test -success-output = "final" -failure-output = "immediate-final" +# show log output from each failing test +failure-output = "final" [profile.all] # A profile to run all tests (including tests that run longer than 10 seconds) From 30e0efef760c3996b9c03b7a652ffdd30cb30a61 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Wed, 3 Jun 2026 03:41:24 -0700 Subject: [PATCH 06/18] review git module remove unused functions transition to git CLI in tests This is a small step toward removing our dependence on libgit2 --- Cargo.lock | 3 +- cpp-linter/Cargo.toml | 8 +- cpp-linter/src/git.rs | 205 ++++++++++-------------------------------- 3 files changed, 56 insertions(+), 160 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c65962ce..56bbe455 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -671,8 +671,7 @@ dependencies = [ [[package]] name = "git-bot-feedback" version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43abc02f0a7c68ab167e3a9b11dbeb7032dec48d5d9097ce19d77a6892b8" +source = "git+https://github.com/2bndy5/git-bot-feedback?branch=git-diff-no-staged#e2143b3a92731fdedeb9cf081132fa93d23555f9" dependencies = [ "async-trait", "chrono", diff --git a/cpp-linter/Cargo.toml b/cpp-linter/Cargo.toml index 12e4daf8..18b39b59 100644 --- a/cpp-linter/Cargo.toml +++ b/cpp-linter/Cargo.toml @@ -22,7 +22,6 @@ clap = { workspace = true, optional = true } colored = { workspace = true, optional = true } fast-glob = "1.0.1" futures = "0.3.32" -git-bot-feedback = { version = "0.5.3", features = ["file-changes"] } git2 = { version = "0.21.0", features = ["https"] } log = { workspace = true } quick-xml = { version = "0.40.1", features = ["serialize"] } @@ -34,6 +33,13 @@ serde_json = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +[dependencies.git-bot-feedback] +# version = "0.5.4" +# path = "../../git-bot-feedback" +git = "https://github.com/2bndy5/git-bot-feedback" +branch = "git-diff-no-staged" +features = ["file-changes"] + [dev-dependencies] mockito = { workspace = true } tempfile = { workspace = true } diff --git a/cpp-linter/src/git.rs b/cpp-linter/src/git.rs index 10055f2e..658ce1c2 100644 --- a/cpp-linter/src/git.rs +++ b/cpp-linter/src/git.rs @@ -8,129 +8,16 @@ //! (str or bytes) only happens in CI or when libgit2 cannot be used to initialize a //! repository. -use std::{fmt::Display, ops::RangeInclusive, path::PathBuf}; +use std::{ops::RangeInclusive, path::PathBuf}; -use anyhow::{Context, Result}; // non-std crates -use git2::{Diff, Error, Patch, Repository}; +use anyhow::Result; +use git2::{Diff, Patch}; // project specific modules/crates use crate::{cli::LinesChangedOnly, common_fs::FileObj}; use git_bot_feedback::{FileFilter, error::DiffError}; -/// This (re-)initializes the repository located in the specified `path`. -/// -/// This is actually not used in CI for file permissions and ownership reasons. -/// Rather this is only (supposed to be) used when executed on a local developer -/// machine. -pub fn open_repo(path: &str) -> Result { - Repository::open(PathBuf::from(path).as_path()) -} - -/// Fetches the SHA1 of the commit for the specified [`git2::Repository`]. -/// -/// The optionally specified `depth` can be used to traverse the tree a number of times -/// since the current `"HEAD"`. -fn get_sha<'d, T: Display>( - repo: &'d Repository, - depth: &Option, -) -> Result, Error> { - match depth { - Some(base) => { - let base = base.to_string(); - // First treat base as an explicit refs/SHAs. If that fails, then - // fall back to `HEAD~` if `base` is purely numeric. - match repo.revparse_single(base.as_str()) { - Ok(obj) => Ok(obj), - Err(err) => { - if base.chars().all(|c| c.is_ascii_digit()) { - repo.revparse_single(format!("HEAD~{}", base).as_str()) - } else { - Err(err) - } - } - } - } - None => repo.revparse_single("HEAD"), - } -} - -/// Fetch the [`git2::Diff`] about a given [`git2::Repository`]. -/// -/// This is actually not used in CI for file permissions and ownership reasons. -/// Rather, this is only (supposed to be) used when executed on a local developer -/// machine. -/// -/// ## Using `diff_base` and `ignore_index` -/// -/// The `diff_base` is a commit or ref to use as the base of the diff. -/// Use `ignore_index` to exclude any staged changes in the local index. -/// -/// | `diff_base` value | Git index state | Scope of diff | -/// |-------------------|-----------------|---------------| -/// | `None` | No staged changes | `HEAD~1..HEAD` | -/// | `None` | Has staged changes | `HEAD..index` | -/// | `Some(2)` or `Some("HEAD~2")` | No staged changes | `HEAD~2..HEAD` | -/// | `Some(2)` or `Some("HEAD~2")` | Has staged changes | `HEAD~2..index` | -pub fn get_diff<'d, T: Display>( - repo: &'d Repository, - diff_base: &Option, - ignore_index: bool, -) -> Result> { - let use_staged_files = if ignore_index { - false - } else { - // check if there are staged file changes - repo.statuses(None) - .with_context(|| "Could not get repo statuses")? - .iter() - .any(|entry| { - entry.status().bits() - & (git2::Status::INDEX_NEW.bits() - | git2::Status::INDEX_MODIFIED.bits() - | git2::Status::INDEX_RENAMED.bits()) - > 0 - }) - }; - let base = if diff_base.is_some() { - // diff base is specified (regardless of staged changes) - get_sha(repo, diff_base) - } else if !use_staged_files { - // diff base is unspecified, when the repo has - // no staged changes (and they are not ignored), - // then focus on just the last commit - get_sha(repo, &Some(1)) - } else { - // diff base is unspecified and there are staged changes, so - // let base be set to HEAD. - get_sha(repo, &None::) - }? - .peel_to_tree()?; - - // RARE BUG when `head` is the first commit in the repo! Affects local-only runs. - // > Error { code: -3, class: 3, message: "parent 0 does not exist" } - if use_staged_files { - // get diff including staged files - repo.diff_tree_to_index(Some(&base), None, None) - .with_context(|| { - format!( - "Could not get diff for {}..index", - &base.id().to_string()[..7] - ) - }) - } else { - // get diff for range of commits between base..HEAD - let head = get_sha(repo, &None::)?.peel_to_tree()?; - repo.diff_tree_to_tree(Some(&base), Some(&head), None) - .with_context(|| { - format!( - "Could not get diff for {}..HEAD", - &base.id().to_string()[..7] - ) - }) - } -} - /// Parses a patch for a single file in a diff. /// /// Returns the list of line numbers that have additions and the ranges spanning each @@ -227,40 +114,56 @@ pub fn parse_diff_from_buf( mod test { use std::{ env::{self, current_dir, set_current_dir}, - fs::read, + fs, + process::Command, }; - use git2::{ApplyLocation, Diff, IndexAddOption, Repository, build::CheckoutBuilder}; use tempfile::{TempDir, tempdir}; - use super::get_sha; use crate::{cli::LinesChangedOnly, rest_client::RestClient}; use git_bot_feedback::FileFilter; const TEST_REPO_URL: &str = "https://github.com/cpp-linter/cpp-linter"; // used to setup a testing stage - fn clone_repo(sha: Option<&str>, path: &str, patch_path: Option<&str>) -> Repository { - let repo = Repository::clone(TEST_REPO_URL, path).unwrap(); + fn clone_repo(sha: Option<&str>, path: &str, patch_path: Option<&str>) { + // let repo = Repository::clone(TEST_REPO_URL, path).unwrap(); + let ok = Command::new("git") + .args(["clone", TEST_REPO_URL, path]) + .status() + .expect("Failed to clone repo"); + if !ok.success() { + panic!("Failed to clone repo"); + } if let Some(sha) = sha { - let commit = repo.revparse_single(sha).unwrap(); - repo.checkout_tree( - &commit, - Some(CheckoutBuilder::new().force().recreate_missing(true)), - ) - .unwrap(); - repo.set_head_detached(commit.id()).unwrap(); + let ok = Command::new("git") + .args(["-c", "advice.detachedHead=false", "checkout", sha]) + .current_dir(path) + .status() + .expect("Failed to checkout commit"); + if !ok.success() { + panic!("Failed to checkout commit"); + } } if let Some(patch) = patch_path { - let diff = Diff::from_buffer(&read(patch).unwrap()).unwrap(); - repo.apply(&diff, ApplyLocation::Both, None).unwrap(); - let mut index = repo.index().unwrap(); - index - .add_all(["tests/demo/demo.*"], IndexAddOption::DEFAULT, None) - .unwrap(); - index.write().unwrap(); + let canonical_path_path = fs::canonicalize(patch).unwrap(); + let ok = Command::new("git") + .args(["apply", "--index", canonical_path_path.to_str().unwrap()]) + .current_dir(path) + .status() + .expect("Failed to apply patch and stage its changes"); + if !ok.success() { + panic!("Failed to apply patch and stage its changes"); + } + let ok = Command::new("git") + .args(["status", "-s"]) + .current_dir(path) + .status() + .expect("Failed to get git status after applying patch"); + if !ok.success() { + panic!("Failed to get git status after applying patch"); + } } - repo } fn get_temp_dir() -> TempDir { @@ -290,7 +193,7 @@ mod test { let file_filter = FileFilter::new(&["target"], extensions, None); set_current_dir(tmp).unwrap(); let base_diff = if ignore_staged { - Some("0".to_string()) + Some("HEAD".to_string()) } else { None }; @@ -395,30 +298,18 @@ mod test { true, ) .await; - println!("files = {:?}", files); - let git_status = std::process::Command::new("git") - .args(["diff", "HEAD~1..HEAD"]) - .output() - .map(|o| String::from_utf8(o.stdout).unwrap()) + Command::new("git") + .args([ + "--no-pager", + "show", + "a2875ac00e6f1dc3eb4ac19712c7a241b5a76e83", + "--format=%b", + ]) + .status() .unwrap(); - eprintln!("git status:\n{git_status}"); eprintln!("files: {files:?}"); assert!(files.is_empty()); set_current_dir(cur_dir).unwrap(); // prep to delete temp_folder drop(tmp); // delete temp_folder } - - #[test] - fn repo_get_sha() { - let tmp_dir = get_temp_dir(); - let repo = clone_repo(None, tmp_dir.path().to_str().unwrap(), None); - for (ours, theirs) in [(None::, "HEAD"), (Some(2), "HEAD~2")] { - let our_obj = get_sha(&repo, &ours).unwrap(); - let their_obj = get_sha(&repo, &Some(theirs)).unwrap(); - assert_eq!(our_obj.id(), their_obj.id()); - } - // test an invalid ref for coverage measurement - assert!(get_sha(&repo, &Some("1.0")).is_err()); - drop(tmp_dir); // delete temp_folder - } } From 73c9c27bce78cb2d59951771208ac55163ea0b1a Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Wed, 3 Jun 2026 05:31:29 -0700 Subject: [PATCH 07/18] strip annoying file protocol prefix for windows canonical path to git test patch asset --- .config/nextest.toml | 3 +++ cpp-linter/src/git.rs | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.config/nextest.toml b/.config/nextest.toml index d7ba456e..2c511395 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -20,6 +20,9 @@ status-level = "skip" # Run this profile before other tests to warmup the cache of clang tools binaries. default-filter = 'test(#*eval_version)' +# only show the job(s) being run in this profile +status-level = "pass" + [profile.ci] # A profile to run only tests that use clang-tidy and/or clang-format # NOTE: This profile is intended to keep CI runtime low. Locally, use default or all profiles diff --git a/cpp-linter/src/git.rs b/cpp-linter/src/git.rs index 658ce1c2..145e3b48 100644 --- a/cpp-linter/src/git.rs +++ b/cpp-linter/src/git.rs @@ -146,9 +146,14 @@ mod test { } } if let Some(patch) = patch_path { - let canonical_path_path = fs::canonicalize(patch).unwrap(); + let canonical_patch_path = fs::canonicalize(patch).unwrap(); + let patch_path = canonical_patch_path + .to_str() + .unwrap() + // on Windows, canonical paths can have a prefix of "\\?\" which git does not recognize + .trim_start_matches("\\\\?\\"); let ok = Command::new("git") - .args(["apply", "--index", canonical_path_path.to_str().unwrap()]) + .args(["apply", "--index", patch_path]) .current_dir(path) .status() .expect("Failed to apply patch and stage its changes"); From 7389a2512e512e12af79a81607445429058610cb Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Wed, 3 Jun 2026 06:37:44 -0700 Subject: [PATCH 08/18] remove reliance on openSSL This project's production code never does `git clone`, so we really shouldn't need the `https` feature of the `git2` crate enabled. --- .github/workflows/binary-builds.yml | 23 +- .github/workflows/node-js-packaging.yml | 74 ++--- .github/workflows/python-packaging.yml | 41 ++- Cargo.lock | 425 +++++++++++++++++------- bindings/node/Cargo.toml | 3 - bindings/python/Cargo.toml | 3 - clang-installer/Cargo.toml | 2 +- cpp-linter/Cargo.toml | 8 +- cspell.config.yml | 1 + 9 files changed, 373 insertions(+), 207 deletions(-) diff --git a/.github/workflows/binary-builds.yml b/.github/workflows/binary-builds.yml index 190fd11c..a70ba1b7 100644 --- a/.github/workflows/binary-builds.yml +++ b/.github/workflows/binary-builds.yml @@ -42,63 +42,48 @@ jobs: include: - target: aarch64-unknown-linux-gnu os: ubuntu-latest - vendored: true cross: true - target: aarch64-unknown-linux-musl os: ubuntu-latest - vendored: true cross: true - target: x86_64-unknown-linux-gnu os: ubuntu-latest - vendored: false cross: false - target: x86_64-unknown-linux-musl os: ubuntu-latest - vendored: true cross: true - target: arm-unknown-linux-gnueabi os: ubuntu-latest - vendored: true cross: true - target: arm-unknown-linux-gnueabihf os: ubuntu-latest - vendored: true cross: true - target: armv7-unknown-linux-gnueabihf os: ubuntu-latest - vendored: true cross: true - target: powerpc-unknown-linux-gnu os: ubuntu-latest - vendored: true cross: true - # - target: powerpc64-unknown-linux-gnu - # os: ubuntu-latest - # vendored: true - # cross: true + - target: powerpc64-unknown-linux-gnu + os: ubuntu-latest + cross: true - target: powerpc64le-unknown-linux-gnu os: ubuntu-latest - vendored: true cross: true - target: s390x-unknown-linux-gnu os: ubuntu-latest - vendored: true cross: true - target: aarch64-apple-darwin os: macos-latest - vendored: true cross: false - target: x86_64-apple-darwin os: macos-latest - vendored: true cross: false - target: x86_64-pc-windows-msvc os: windows-latest - vendored: false cross: false - target: aarch64-pc-windows-msvc os: windows-latest - vendored: false cross: false runs-on: ${{ matrix.os }} permissions: @@ -136,7 +121,7 @@ jobs: --bin clang-tools --release --target ${{ matrix.target }} - --features ${{ matrix.vendored && 'bin,openssl-vendored' || 'bin' }} + --features bin - name: Prepare artifacts (unix) if: runner.os != 'Windows' diff --git a/.github/workflows/node-js-packaging.yml b/.github/workflows/node-js-packaging.yml index 3abadf57..86ba413f 100644 --- a/.github/workflows/node-js-packaging.yml +++ b/.github/workflows/node-js-packaging.yml @@ -45,7 +45,7 @@ jobs: settings: - host: macos-latest target: x86_64-apple-darwin - build: yarn build --target x86_64-apple-darwin --features openssl-vendored + build: yarn build --target x86_64-apple-darwin - host: windows-latest build: yarn build --target x86_64-pc-windows-msvc target: x86_64-pc-windows-msvc @@ -54,22 +54,22 @@ jobs: target: i686-pc-windows-msvc - host: ubuntu-latest target: x86_64-unknown-linux-gnu - build: yarn build --target x86_64-unknown-linux-gnu --use-cross --features openssl-vendored + build: yarn build --target x86_64-unknown-linux-gnu --use-cross - host: ubuntu-latest target: x86_64-unknown-linux-musl - build: yarn build --target x86_64-unknown-linux-musl -x --features openssl-vendored + build: yarn build --target x86_64-unknown-linux-musl -x - host: macos-latest target: aarch64-apple-darwin - build: yarn build --target aarch64-apple-darwin --features openssl-vendored + build: yarn build --target aarch64-apple-darwin - host: ubuntu-latest target: aarch64-unknown-linux-gnu - build: yarn build --target aarch64-unknown-linux-gnu --use-cross --features openssl-vendored + build: yarn build --target aarch64-unknown-linux-gnu --use-cross - host: ubuntu-latest target: armv7-unknown-linux-gnueabihf - build: yarn build --target armv7-unknown-linux-gnueabihf --use-cross --features openssl-vendored + build: yarn build --target armv7-unknown-linux-gnueabihf --use-cross - host: ubuntu-latest target: aarch64-unknown-linux-musl - build: yarn build --target aarch64-unknown-linux-musl -x --features openssl-vendored + build: yarn build --target aarch64-unknown-linux-musl -x - host: windows-latest target: aarch64-pc-windows-msvc build: yarn build --target aarch64-pc-windows-msvc @@ -100,20 +100,20 @@ jobs: target/ key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }} - uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1 - if: ${{ contains(matrix.settings.build, ' -x ') }} + if: ${{ endsWith(matrix.settings.build, ' -x') }} with: version: 0.14.1 use-cache: true # zizmor: ignore[cache-poisoning] - name: Install cargo-binstall - if: contains(matrix.settings.build, ' -x ') || contains(matrix.settings.build, '--use-cross ') + if: endsWith(matrix.settings.build, ' -x') || endsWith(matrix.settings.build, '--use-cross') uses: cargo-bins/cargo-binstall@aaa84a43aec4955a42c5ffc65d258961e39f276e # v1.19.1 env: GITHUB_TOKEN: ${{ github.token }} - - name: Install ${{contains(matrix.settings.build, ' -x ') && 'cargo-zigbuild' || 'cargo-cross'}} - if: contains(matrix.settings.build, ' -x ') || contains(matrix.settings.build, '--use-cross ') + - name: Install ${{endsWith(matrix.settings.build, ' -x') && 'cargo-zigbuild' || 'cargo-cross'}} + if: endsWith(matrix.settings.build, ' -x') || endsWith(matrix.settings.build, '--use-cross') env: GITHUB_TOKEN: ${{ github.token }} - tool: ${{contains(matrix.settings.build, ' -x ') && 'cargo-zigbuild' || 'cross'}} + tool: ${{endsWith(matrix.settings.build, ' -x') && 'cargo-zigbuild' || 'cross'}} run: cargo binstall -y $tool - name: Setup toolchain run: ${{ matrix.settings.setup }} @@ -139,8 +139,7 @@ jobs: - uses: actions/checkout@v6 with: persist-credentials: false - - name: Build - id: build + - name: setup FreeBSD VM uses: cross-platform-actions/action@0c165ad7eb2d6a7e8552d6af5aad2bbedfc646b0 # v1.1.0 env: DEBUG: napi:* @@ -151,29 +150,30 @@ jobs: memory: 8G cpu_count: 3 environment_variables: DEBUG RUSTUP_IO_THREADS - shell: bash - run: | - sudo pkg install -y -f curl node libnghttp2 npm cmake - sudo npm install -g yarn --ignore-scripts - curl https://sh.rustup.rs -sSf --output rustup.sh - sh rustup.sh -y --profile minimal --default-toolchain stable - source "$HOME/.cargo/env" - echo "~~~~ rustc --version ~~~~" - rustc --version - echo "~~~~ node -v ~~~~" - node -v - echo "~~~~ yarn --version ~~~~" - yarn --version - pwd - ls -lah - whoami - env - freebsd-version - yarn install - yarn build - rm -rf node_modules - rm -rf target - rm -rf .yarn/cache + - name: Build + shell: cpa.sh {0} + run: |- + sudo pkg install -y -f curl node libnghttp2 npm cmake + sudo npm install -g yarn --ignore-scripts + curl https://sh.rustup.rs -sSf --output rustup.sh + sh rustup.sh -y --profile minimal --default-toolchain stable + . "$HOME/.cargo/env" + echo "~~~~ rustc --version ~~~~" + rustc --version + echo "~~~~ node -v ~~~~" + node -v + echo "~~~~ yarn --version ~~~~" + yarn --version + pwd + ls -lah + whoami + env + freebsd-version + yarn install + yarn build + rm -rf node_modules + rm -rf target + rm -rf .yarn/cache - name: Upload artifact uses: actions/upload-artifact@v7 with: diff --git a/.github/workflows/python-packaging.yml b/.github/workflows/python-packaging.yml index 18b8b87a..196064a4 100644 --- a/.github/workflows/python-packaging.yml +++ b/.github/workflows/python-packaging.yml @@ -72,34 +72,33 @@ jobs: with: python-version: '3.x' - - name: Calculate openssl-vendored - shell: bash - id: is-openssl-vendored - run: | - if [[ "${{ startsWith(matrix.platform.target, 'x86') }}" == "true" ]]; then - echo "enabled=" >> $GITHUB_OUTPUT - else - echo "enabled=--features openssl-vendored" >> $GITHUB_OUTPUT - fi - - name: Build wheels uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: target: ${{ matrix.platform.target }} - args: --release --out dist --find-interpreter ${{ steps.is-openssl-vendored.outputs.enabled }} - # sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} # zizmor: ignore[cache-poisoning] + # Use the zig toolchain for all targets except ppc64le. + # Note, this is meant to avoid build errors in aws-lc-sys crate, which compiles C code. + args: >- + --release --out dist --find-interpreter + ${{ matrix.platform.target != 'ppc64le' && '--zig' || ''}} + # avoid cache-poisoning potential for actually distributed builds + sccache: ${{ startsWith(github.ref, 'refs/tags') && 'false' || 'true' }} manylinux: auto + # here we either install the ppc64le compiler/toolchain or install zig on all others before-script-linux: | case "${{ matrix.platform.target }}" in - "aarch64" | "armv7" | "s390x" | "ppc64le") - # NOTE: pypa/manylinux docker images are Debian based - sudo apt-get update - sudo apt-get install -y pkg-config libssl-dev + ppc64le) + apt-get update + apt-get install -y \ + pkg-config \ + gcc-powerpc64le-linux-gnu \ + g++-powerpc64le-linux-gnu \ + binutils-powerpc64le-linux-gnu \ + libc6-dev-ppc64el-cross ;; - "x86" | "x86_64") - # NOTE: rust-cross/manylinux docker images are CentOS based - yum update -y - yum install -y openssl openssl-devel + *) + # spell-checker: disable-next-line + pip install ziglang ;; esac - name: Upload wheels @@ -158,7 +157,7 @@ jobs: uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: target: ${{ matrix.platform.target }} - args: --release --out dist --find-interpreter --features openssl-vendored + args: --release --out dist --find-interpreter sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} # zizmor: ignore[cache-poisoning] - name: Upload wheels uses: actions/upload-artifact@v7 diff --git a/Cargo.lock b/Cargo.lock index 56bbe455..226df9b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,6 +121,28 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "base64" version = "0.22.1" @@ -181,6 +203,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.44" @@ -268,6 +296,15 @@ dependencies = [ "pyo3", ] +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -283,6 +320,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "convert_case" version = "0.11.0" @@ -442,6 +489,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -510,21 +563,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -534,6 +572,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.32" @@ -639,8 +683,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -650,9 +696,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -696,9 +744,6 @@ dependencies = [ "libc", "libgit2-sys", "log", - "openssl-probe 0.1.6", - "openssl-sys", - "url", ] [[package]] @@ -823,22 +868,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.20" @@ -1027,6 +1056,55 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -1070,7 +1148,6 @@ dependencies = [ "cc", "libc", "libz-sys", - "openssl-sys", "pkg-config", ] @@ -1132,6 +1209,12 @@ version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "memchr" version = "2.8.1" @@ -1248,23 +1331,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "native-tls" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe 0.2.1", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nohash-hasher" version = "0.2.0" @@ -1292,65 +1358,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "openssl" -version = "0.10.80" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - [[package]] name = "openssl-probe" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" -[[package]] -name = "openssl-src" -version = "300.6.0+3.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" -dependencies = [ - "cc", -] - -[[package]] -name = "openssl-sys" -version = "0.9.116" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" -dependencies = [ - "cc", - "libc", - "openssl-src", - "pkg-config", - "vcpkg", -] - [[package]] name = "option-ext" version = "0.2.0" @@ -1509,6 +1522,62 @@ dependencies = [ "serde", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.45" @@ -1624,18 +1693,19 @@ dependencies = [ "http-body-util", "hyper", "hyper-rustls", - "hyper-tls", "hyper-util", "js-sys", "log", "mime", - "native-tls", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pki-types", + "rustls-platform-verifier", "sync_wrapper", "tokio", - "tokio-native-tls", + "tokio-rustls", "tower", "tower-http", "tower-service", @@ -1665,6 +1735,15 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.4" @@ -1684,6 +1763,7 @@ version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ + "aws-lc-rs", "once_cell", "rustls-pki-types", "rustls-webpki", @@ -1691,21 +1771,62 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pki-types" version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ + "web-time", "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -1723,6 +1844,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.29" @@ -1845,6 +1975,22 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "similar" version = "2.7.0" @@ -1992,6 +2138,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.52.3" @@ -2019,16 +2180,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.4" @@ -2194,6 +2345,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -2326,6 +2487,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" version = "8.0.2" @@ -2335,6 +2515,15 @@ dependencies = [ "libc", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "windows-core" version = "0.62.2" diff --git a/bindings/node/Cargo.toml b/bindings/node/Cargo.toml index 7ae566a8..315fc3d9 100644 --- a/bindings/node/Cargo.toml +++ b/bindings/node/Cargo.toml @@ -22,8 +22,5 @@ napi-derive = "3.5.6" cpp-linter = { path = "../../cpp-linter", features = ["bin"] } anyhow = { workspace = true } -[features] -openssl-vendored = ["cpp-linter/openssl-vendored"] - [build-dependencies] napi-build = "2.3.2" diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml index 75458e5b..77b47d1b 100644 --- a/bindings/python/Cargo.toml +++ b/bindings/python/Cargo.toml @@ -20,6 +20,3 @@ bench = false pyo3 = { workspace = true, features = ["abi3-py39"] } cpp-linter = { path = "../../cpp-linter", features = ["bin"] } tokio = { workspace = true } - -[features] -openssl-vendored = ["cpp-linter/openssl-vendored"] diff --git a/clang-installer/Cargo.toml b/clang-installer/Cargo.toml index 9a692a98..69856eba 100644 --- a/clang-installer/Cargo.toml +++ b/clang-installer/Cargo.toml @@ -41,7 +41,7 @@ tempfile = { workspace = true } [dev-dependencies] colored = { workspace = true } mockito = { workspace = true } -reqwest = { workspace = true, features = ["native-tls"] } +reqwest = { workspace = true, features = ["default-tls"] } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } [features] diff --git a/cpp-linter/Cargo.toml b/cpp-linter/Cargo.toml index 18b39b59..c08f5e86 100644 --- a/cpp-linter/Cargo.toml +++ b/cpp-linter/Cargo.toml @@ -22,7 +22,7 @@ clap = { workspace = true, optional = true } colored = { workspace = true, optional = true } fast-glob = "1.0.1" futures = "0.3.32" -git2 = { version = "0.21.0", features = ["https"] } +git2 = { version = "0.21.0" } log = { workspace = true } quick-xml = { version = "0.40.1", features = ["serialize"] } regex = { workspace = true } @@ -43,12 +43,10 @@ features = ["file-changes"] [dev-dependencies] mockito = { workspace = true } tempfile = { workspace = true } -git2 = { version = "0.21.0", features = ["https"] } -reqwest = { workspace = true, features = ["native-tls"] } +reqwest = { workspace = true, features = ["default-tls"] } [features] -openssl-vendored = ["git2/vendored-openssl", "reqwest/native-tls-vendored"] -bin = ["reqwest/native-tls", "dep:clap", "dep:colored"] +bin = ["reqwest/default-tls", "dep:clap", "dep:colored"] [lib] bench = false diff --git a/cspell.config.yml b/cspell.config.yml index 76ac65fa..66b44c25 100644 --- a/cspell.config.yml +++ b/cspell.config.yml @@ -4,6 +4,7 @@ words: - armv - bindgen - binstall + - binutils - bndy - Boolish - bugprone From e4f7145bb2416c17f3e2655cd11690c91169f47e Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Wed, 3 Jun 2026 07:15:28 -0700 Subject: [PATCH 09/18] rename and alias a couple nur tasks --- .github/workflows/run-dev-tests.yml | 2 +- CONTRIBUTING.md | 2 +- nurfile | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-dev-tests.yml b/.github/workflows/run-dev-tests.yml index 0e0cb883..f8cb869e 100644 --- a/.github/workflows/run-dev-tests.yml +++ b/.github/workflows/run-dev-tests.yml @@ -117,7 +117,7 @@ jobs: } - name: Generate Coverage HTML report - run: nur test llvm-cov + run: nur test html - name: Upload coverage data uses: actions/upload-artifact@v7 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 23539d3c..d4a3e7f3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,7 @@ nur test --profile ci To generate a coverage report: ```sh -nur test llvm-cov --open +nur test html --open ``` The `--open` flag is optional. It opens the built coverage report in your diff --git a/nurfile b/nurfile index d818d962..bffda157 100644 --- a/nurfile +++ b/nurfile @@ -87,7 +87,7 @@ export def "nur test" [ } # Generate HTML coverage report -export def "nur test llvm-cov" [ +export def "nur test html" [ --open (-o), # Open the built report in your browser. ] { mut cmd = [ @@ -150,3 +150,5 @@ export def "nur pre-commit" [ let args = if $all { ["--all-files"] } else { [] } run-cmd uv run pre-commit run ...$args } + +alias "nur pre" = nur pre-commit From b8539a723b19473b9c5afa2d9086125ad1dd151a Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Wed, 3 Jun 2026 07:24:50 -0700 Subject: [PATCH 10/18] switch back to git-bot-feedback v0.5.4 --- Cargo.lock | 9 +++++---- cpp-linter/Cargo.toml | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 226df9b6..f135d8b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -718,8 +718,9 @@ dependencies = [ [[package]] name = "git-bot-feedback" -version = "0.5.3" -source = "git+https://github.com/2bndy5/git-bot-feedback?branch=git-diff-no-staged#e2143b3a92731fdedeb9cf081132fa93d23555f9" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "002bc7450062bd6651a86e99a454f52aea873bfe293d24cfabbf52180c97da89" dependencies = [ "async-trait", "chrono", @@ -1811,7 +1812,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2521,7 +2522,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/cpp-linter/Cargo.toml b/cpp-linter/Cargo.toml index c08f5e86..51656490 100644 --- a/cpp-linter/Cargo.toml +++ b/cpp-linter/Cargo.toml @@ -34,11 +34,11 @@ thiserror = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } [dependencies.git-bot-feedback] -# version = "0.5.4" -# path = "../../git-bot-feedback" -git = "https://github.com/2bndy5/git-bot-feedback" -branch = "git-diff-no-staged" +version = "0.5.4" features = ["file-changes"] +# path = "../../git-bot-feedback" +# git = "https://github.com/2bndy5/git-bot-feedback" +# branch = "git-diff-no-staged" [dev-dependencies] mockito = { workspace = true } From c25e1947e3df1045bf8426835cb89fb98e0d4520 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Wed, 3 Jun 2026 07:42:30 -0700 Subject: [PATCH 11/18] remove local GH action Installing clang tools is now part of the tested production code. We don't need a dedicated GH action to do that anymore. --- .github/install-clang-action/README.md | 32 ---------------- .github/install-clang-action/action.yml | 50 ------------------------- 2 files changed, 82 deletions(-) delete mode 100644 .github/install-clang-action/README.md delete mode 100644 .github/install-clang-action/action.yml diff --git a/.github/install-clang-action/README.md b/.github/install-clang-action/README.md deleted file mode 100644 index 6be75df2..00000000 --- a/.github/install-clang-action/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# cpp-linter/cpp-linter-rs/install-clang-action - -This is action is not meant to be published in the GitHub marketplace. -It intended to be a repo-specific action for installing multiple versions of -clang-format and clang-tidy in a single workflow run. - -This is used in the cpp-linter rust tests only. - -## Example - -```yml -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Install clang tools v16 - uses: cpp-linter/cpp-linter-rs/install-clang-action@main - with: - version: 16 - - name: test with clang tools v16 - env: - CLANG_VERSION: 16 - run: just test - - name: Install clang tools v17 - uses: cpp-linter/cpp-linter-rs/install-clang-action@main - with: - version: 17 - - name: test with clang tools v17 - env: - CLANG_VERSION: 17 - run: just test -``` diff --git a/.github/install-clang-action/action.yml b/.github/install-clang-action/action.yml deleted file mode 100644 index 92d81d3a..00000000 --- a/.github/install-clang-action/action.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Install clang -description: Install clang tidy and clang-format given a specified version - -inputs: - version: - description: The version of clang tools to install - required: true - -runs: - using: composite - steps: - - name: Install Linux clang dependencies - if: runner.os == 'Linux' - shell: bash - env: - CLANG_VERSION: ${{ inputs.version }} - # NOTE: sudo apt-get update should be executed at least once before running this action - run: | - # First try installing from default Ubuntu repositories before trying LLVM script - if ! sudo apt-get install -y clang-format-${CLANG_VERSION} clang-tidy-${CLANG_VERSION}; then - if [ ! -f "${{ runner.temp }}/llvm_install.sh" ]; then - # This LLVM script will add the relevant LLVM PPA: https://apt.llvm.org/ - wget https://apt.llvm.org/llvm.sh -O ${{ runner.temp }}/llvm_install.sh - chmod +x ${{ runner.temp }}/llvm_install.sh - fi - if sudo "${{ runner.temp }}/llvm_install.sh" "${CLANG_VERSION}" ; then - sudo apt-get install -y clang-format-${CLANG_VERSION} clang-tidy-${CLANG_VERSION} - fi - # remove the PPA for future reuse of install script (regardless of successful install) - sudo rm /etc/apt/sources.list.d/*llvm*.list || true - fi - - - name: Install MacOS clang dependencies - if: runner.os == 'macOS' - shell: bash - continue-on-error: true - env: - CLANG_VERSION: ${{ inputs.version }} - run: | - brew update - brew install llvm@${CLANG_VERSION} - ln -s "$(brew --prefix llvm@${CLANG_VERSION})/bin/clang-format" "/usr/local/bin/clang-format-${CLANG_VERSION}" - ln -s "$(brew --prefix llvm@${CLANG_VERSION})/bin/clang-tidy" "/usr/local/bin/clang-tidy-${CLANG_VERSION}" - - name: Use clang-tools-pip as fallback - shell: bash - env: - CLANG_VERSION: ${{ inputs.version }} - run: | - python -m pip install clang-tools - clang-tools -i ${CLANG_VERSION} -b -f From 1de77f3e6645e84429c8d04e8824ddbc45392659 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Wed, 3 Jun 2026 08:51:29 -0700 Subject: [PATCH 12/18] reinstate test where bad_pr_info causes panic --- cpp-linter/tests/reviews.rs | 55 +++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/cpp-linter/tests/reviews.rs b/cpp-linter/tests/reviews.rs index f18c7557..b13c16ac 100644 --- a/cpp-linter/tests/reviews.rs +++ b/cpp-linter/tests/reviews.rs @@ -72,15 +72,19 @@ fn generate_tool_summary(review_enabled: bool, force_lgtm: bool, tool_name: &str async fn setup(lib_root: &Path, test_params: &TestParams) { let mut event_payload_path = NamedTempFile::new_in("./").unwrap(); - let event_payload = serde_json::json!({ - "pull_request": { - "draft": test_params.pr_draft, - "state": test_params.pr_state, - "number": PR, - "locked": false, - } - }) - .to_string(); + let event_payload = if test_params.bad_pr_info { + "".to_string() + } else { + serde_json::json!({ + "pull_request": { + "draft": test_params.pr_draft, + "state": test_params.pr_state, + "number": PR, + "locked": false, + } + }) + .to_string() + }; event_payload_path .write_all(event_payload.as_bytes()) .expect("Failed to create mock event payload."); @@ -108,22 +112,24 @@ async fn setup(lib_root: &Path, test_params: &TestParams) { } let mut mocks = vec![]; - let pr_endpoint = format!("/repos/{REPO}/pulls/{PR}"); - mocks.push( - server - .mock("GET", format!("{pr_endpoint}/files").as_str()) - .match_header("Accept", "application/vnd.github.raw+json") - .match_header("Authorization", format!("token {TOKEN}").as_str()) - .match_query(Matcher::Any) - .with_body_from_file(format!("{asset_path}pr_27.json")) - .with_header(REMAINING_RATE_LIMIT_HEADER, "50") - .with_header(RESET_RATE_LIMIT_HEADER, reset_timestamp.as_str()) - .create(), - ); + if !test_params.bad_pr_info { + let pr_endpoint = format!("/repos/{REPO}/pulls/{PR}"); + mocks.push( + server + .mock("GET", format!("{pr_endpoint}/files").as_str()) + .match_header("Accept", "application/vnd.github.raw+json") + .match_header("Authorization", format!("token {TOKEN}").as_str()) + .match_query(Matcher::Any) + .with_body_from_file(format!("{asset_path}pr_27.json")) + .with_header(REMAINING_RATE_LIMIT_HEADER, "50") + .with_header(RESET_RATE_LIMIT_HEADER, reset_timestamp.as_str()) + .create(), + ); + } let reviews_endpoint = format!("/repos/{REPO}/pulls/{PR}/reviews"); - if test_params.pr_state != "closed" { + if test_params.pr_state != "closed" && !test_params.bad_pr_info { let mut mock = server .mock("GET", reviews_endpoint.as_str()) .match_header("Accept", "application/vnd.github.raw+json") @@ -149,6 +155,7 @@ async fn setup(lib_root: &Path, test_params: &TestParams) { if !test_params.fail_get_existing_reviews && !test_params.bad_existing_reviews && test_params.pr_state != "closed" + && !test_params.bad_pr_info { mocks.push( server @@ -247,12 +254,12 @@ async fn setup(lib_root: &Path, test_params: &TestParams) { } match run_main(args).await { Ok(_) => { - if test_params.bad_existing_reviews { + if test_params.bad_existing_reviews || test_params.bad_pr_info { panic!("Expected failure, but it succeeded"); } } Err(e) => { - if !test_params.bad_existing_reviews { + if !test_params.bad_existing_reviews && !test_params.bad_pr_info { panic!("Failed unexpectedly: {e:?}"); } } From 083965d7611bd6b6ac0a69e06504b0aa810ceffb Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Wed, 3 Jun 2026 08:52:15 -0700 Subject: [PATCH 13/18] fix 1 comment; remove another --- cpp-linter/src/cli/structs.rs | 2 +- cpp-linter/src/run.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cpp-linter/src/cli/structs.rs b/cpp-linter/src/cli/structs.rs index b51605db..b975ebcc 100644 --- a/cpp-linter/src/cli/structs.rs +++ b/cpp-linter/src/cli/structs.rs @@ -220,7 +220,7 @@ impl From<&Cli> for ClangParams { } /// A struct to contain CLI options that relate to -/// [`ResApiClient.post_feedback()`](fn@crate::rest_api::ResApiClient.post_feedback()). +/// [`RestClient.post_feedback()`](fn@crate::rest_api::RestClient.post_feedback()). pub struct FeedbackInput { pub thread_comments: ThreadComments, pub no_lgtm: bool, diff --git a/cpp-linter/src/run.rs b/cpp-linter/src/run.rs index 65ebcc9a..224e92f9 100644 --- a/cpp-linter/src/run.rs +++ b/cpp-linter/src/run.rs @@ -78,7 +78,6 @@ pub async fn run_main(args: Vec) -> Result<()> { LevelFilter::Info }, ); - // log::info!("Processing event {}", rest_api_client.event_name); let is_pr = rest_api_client.is_pr(); let mut file_filter = FileFilter::new( From b91685b23c86c8150f0816a5a9437ffefb24ed94 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Wed, 3 Jun 2026 08:53:54 -0700 Subject: [PATCH 14/18] update Python CI global comment and re-enable s390x builds --- .github/workflows/python-packaging.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/python-packaging.yml b/.github/workflows/python-packaging.yml index 196064a4..db31e3b1 100644 --- a/.github/workflows/python-packaging.yml +++ b/.github/workflows/python-packaging.yml @@ -5,11 +5,7 @@ # # NOTE: There are some customizations added to the generated output. # - workflow name is more descriptive -# - use openssl-vendored cargo feature on -# * most cross-compiled Linux wheels -# * all MacOS builds -# - musl linux builds (& x390 Linux target) are excluded due to complexity of -# installing openSSL lib in cross-compiler's docker/env. +# - use zig compiler on most cross-compiled Linux wheels name: Python builds on: @@ -60,8 +56,8 @@ jobs: target: aarch64 - runner: ubuntu-latest target: armv7 - # - runner: ubuntu-latest - # target: s390x + - runner: ubuntu-latest + target: s390x - runner: ubuntu-latest target: ppc64le steps: From 8a49d776b5ba7c976a1083c2d15ee1ea356f7ac9 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Wed, 3 Jun 2026 08:55:20 -0700 Subject: [PATCH 15/18] AI code suggestions about saturating subtraction and collapsing an else if logic paths --- cpp-linter/src/rest_client/mod.rs | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/cpp-linter/src/rest_client/mod.rs b/cpp-linter/src/rest_client/mod.rs index dc1f5c18..f2b97d11 100644 --- a/cpp-linter/src/rest_client/mod.rs +++ b/cpp-linter/src/rest_client/mod.rs @@ -190,12 +190,10 @@ impl RestClient { let has_changes = review_comments.full_patch.iter().any(|p| !p.is_empty()); options.action = if feedback_inputs.passive_reviews { ReviewAction::Comment + } else if options.comments.is_empty() && !has_changes { + ReviewAction::Approve } else { - if options.comments.is_empty() && !has_changes { - ReviewAction::Approve - } else { - ReviewAction::RequestChanges - } + ReviewAction::RequestChanges }; options.summary = review_comments.summarize(&clang_versions, &options.comments); self.client.post_pr_review(&options).await?; @@ -309,8 +307,8 @@ impl RestClient { files, &mut comment, format_checks_failed, - // tidy_version should be `Some()` value at this point. - &clang_versions.tidy_version.as_ref().unwrap().to_string(), + // format_version should be `Some()` value at this point. + &clang_versions.format_version.as_ref().unwrap().to_string(), &mut remaining_length, ); } @@ -319,8 +317,8 @@ impl RestClient { files, &mut comment, tidy_checks_failed, - // format_version should be `Some()` value at this point. - &clang_versions.format_version.as_ref().unwrap().to_string(), + // tidy_version should be `Some()` value at this point. + &clang_versions.tidy_version.as_ref().unwrap().to_string(), &mut remaining_length, ); } @@ -332,6 +330,9 @@ impl RestClient { } } +/// A closing tag for details blocks in markdown comments. +const CLOSER: &str = "\n
"; + fn make_format_comment( files: &[Arc>], comment: &mut String, @@ -342,9 +343,8 @@ fn make_format_comment( let opener = format!( "\n
clang-format (v{version_used}) reports: {format_checks_failed} file(s) not formatted\n\n", ); - let closer = String::from("\n
"); let mut format_comment = String::new(); - *remaining_length -= opener.len() as u64 + closer.len() as u64; + *remaining_length = remaining_length.saturating_sub(opener.len() as u64 + CLOSER.len() as u64); for file in files { let file = file.lock().unwrap(); if let Some(format_advice) = &file.format_advice @@ -360,7 +360,7 @@ fn make_format_comment( } comment.push_str(&opener); comment.push_str(&format_comment); - comment.push_str(&closer); + comment.push_str(CLOSER); } fn make_tidy_comment( @@ -373,9 +373,8 @@ fn make_tidy_comment( let opener = format!( "\n
clang-tidy (v{version_used}) reports: {tidy_checks_failed} concern(s)\n\n" ); - let closer = String::from("\n
"); let mut tidy_comment = String::new(); - *remaining_length -= opener.len() as u64 + closer.len() as u64; + *remaining_length = remaining_length.saturating_sub(opener.len() as u64 + CLOSER.len() as u64); for file in files { let file = file.lock().unwrap(); if let Some(tidy_advice) = &file.tidy_advice { @@ -409,7 +408,7 @@ fn make_tidy_comment( } comment.push_str(&opener); comment.push_str(&tidy_comment); - comment.push_str(&closer); + comment.push_str(CLOSER); } #[cfg(all(test, feature = "bin"))] From fdf87059632ee6bb7033f85bcef1c92edbc63357 Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Wed, 3 Jun 2026 09:49:06 -0700 Subject: [PATCH 16/18] expand test filter in ci profile should remove some tests that are not dependent on clang tools. --- .config/nextest.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.config/nextest.toml b/.config/nextest.toml index 2c511395..24db51b4 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -27,8 +27,7 @@ status-level = "pass" # A profile to run only tests that use clang-tidy and/or clang-format # NOTE: This profile is intended to keep CI runtime low. Locally, use default or all profiles -# This is all tests in tests/ folder + unit test for --extra-args. -default-filter = "kind(test) + test(#*use_extra_args) + test(#*eval_version)" +default-filter = "(kind(test) + test(use_extra_args)) - package(clang-installer)" # show log output from each failing test failure-output = "final" From f60737ae112c65191b085b089f9666402d0c87ee Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Wed, 3 Jun 2026 17:29:18 -0700 Subject: [PATCH 17/18] run all tests on min clang version only clang-related tests run on other clang versions. should short-circuit the 20 minute wait just to see if all tests pass in CI. --- .github/workflows/run-dev-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-dev-tests.yml b/.github/workflows/run-dev-tests.yml index f8cb869e..bf95afcd 100644 --- a/.github/workflows/run-dev-tests.yml +++ b/.github/workflows/run-dev-tests.yml @@ -109,7 +109,7 @@ jobs: with-env { CLANG_VERSION: $"($ver)" } { nur test --profile warmup let test_profile = ( - if ($ver == $max_ver) { "all" } else { "ci" } + if ($ver == $min_ver) { "all" } else { "ci" } ) nur test --profile $test_profile } From 8cb625477c7e2a828f6352dd836e9936cdd6c92d Mon Sep 17 00:00:00 2001 From: Brendan <2bndy5@gmail.com> Date: Wed, 3 Jun 2026 17:44:27 -0700 Subject: [PATCH 18/18] coverage review Looks like the all production code in the git module is no longer actually used, not in tests nor production. So what remains is just the regressions tests. refactored the new rest_client logic a bit, mainly just removing lines that are never triggered by tests. --- cpp-linter/src/git.rs | 124 +----------------------------- cpp-linter/src/rest_client/mod.rs | 51 ++++-------- 2 files changed, 19 insertions(+), 156 deletions(-) diff --git a/cpp-linter/src/git.rs b/cpp-linter/src/git.rs index 145e3b48..a77cf7d0 100644 --- a/cpp-linter/src/git.rs +++ b/cpp-linter/src/git.rs @@ -1,114 +1,8 @@ -//! This module is primarily used to parse diff blobs. +//! This module was primarily used to parse diff blobs. //! -//! It can also be used (locally) to get a list of files changes from either the last -//! commit or the next commit's staging area. -//! -//! This also includes a private module that is used as a fallback (brute force) -//! mechanism when parsing diffs fail using libgit2. NOTE: parsing a diff from a buffer -//! (str or bytes) only happens in CI or when libgit2 cannot be used to initialize a -//! repository. - -use std::{ops::RangeInclusive, path::PathBuf}; - -// non-std crates -use anyhow::Result; -use git2::{Diff, Patch}; - -// project specific modules/crates -use crate::{cli::LinesChangedOnly, common_fs::FileObj}; -use git_bot_feedback::{FileFilter, error::DiffError}; - -/// Parses a patch for a single file in a diff. -/// -/// Returns the list of line numbers that have additions and the ranges spanning each -/// chunk present in the `patch`. -fn parse_patch(patch: &Patch) -> (Vec, Vec>) { - let mut additions = Vec::new(); - let mut diff_hunks = Vec::new(); - for hunk_idx in 0..patch.num_hunks() { - let (hunk, line_count) = patch.hunk(hunk_idx).unwrap(); - diff_hunks.push(RangeInclusive::new( - hunk.new_start(), - hunk.new_start() + hunk.new_lines(), - )); - for line in 0..line_count { - let diff_line = patch.line_in_hunk(hunk_idx, line).unwrap(); - if diff_line.origin_value() == git2::DiffLineType::Addition { - additions.push(diff_line.new_lineno().unwrap()); - } - } - } - (additions, diff_hunks) -} - -/// Parses a given [`git2::Diff`] and returns a list of [`FileObj`]s. -/// -/// The `lines_changed_only` parameter is used to expedite the process and only -/// focus on files that have relevant changes. The `file_filter` parameter applies -/// a filter to only include source files (or ignored files) based on the -/// extensions and ignore patterns specified. -pub fn parse_diff( - diff: &git2::Diff, - file_filter: &FileFilter, - lines_changed_only: &LinesChangedOnly, -) -> Vec { - let mut files: Vec = Vec::new(); - for file_idx in 0..diff.deltas().count() { - let diff_delta = diff.get_delta(file_idx).unwrap(); - let file_path = diff_delta.new_file().path().unwrap().to_path_buf(); - if matches!( - diff_delta.status(), - git2::Delta::Added | git2::Delta::Modified | git2::Delta::Renamed, - ) && file_filter.is_qualified(&file_path) - { - let (added_lines, diff_chunks) = - parse_patch(&Patch::from_diff(diff, file_idx).unwrap().unwrap()); - if lines_changed_only.is_change_valid(!added_lines.is_empty(), !diff_chunks.is_empty()) - { - files.push(FileObj::from(file_path, added_lines, diff_chunks)); - } - } - } - files -} - -/// Same as [`parse_diff`] but takes a buffer of bytes instead of a [`git2::Diff`]. -/// -/// In the case that libgit2 fails to parse the buffer of bytes, a private algorithm is -/// used. In such a case, brute force parsing the diff as a string can be costly. So, a -/// log warning and error are output when this occurs. Please report this instance for -/// troubleshooting/diagnosis as this likely means the diff is malformed or there is a -/// bug in libgit2 source. -pub fn parse_diff_from_buf( - buff: &[u8], - file_filter: &FileFilter, - lines_changed_only: &LinesChangedOnly, -) -> Result, DiffError> { - if let Ok(diff_obj) = &Diff::from_buffer(buff) { - Ok(parse_diff(diff_obj, file_filter, lines_changed_only)) - } else { - log::warn!("libgit2 failed to parse the diff"); - Ok(git_bot_feedback::parse_diff( - &String::from_utf8_lossy(buff), - file_filter, - &lines_changed_only.clone().into(), - )? - .iter() - .map(|(name, diff_lines)| { - let diff_chunks = diff_lines - .diff_hunks - .iter() - .map(|hunk| hunk.start..=hunk.end) - .collect(); - FileObj::from( - PathBuf::from(&name), - diff_lines.added_lines.clone(), - diff_chunks, - ) - }) - .collect()) - } -} +//! Since migrating to git-bot-feedback crate, this module is now purely for regression testing. +//! Any logic that parses diffs from 2 text blobs has been moved to git-bot-feedback. +//! Some diff creation/parsing logic remains in clang_tools/mod.rs module using libgit2 API instead. #[cfg(test)] mod test { @@ -127,7 +21,6 @@ mod test { // used to setup a testing stage fn clone_repo(sha: Option<&str>, path: &str, patch_path: Option<&str>) { - // let repo = Repository::clone(TEST_REPO_URL, path).unwrap(); let ok = Command::new("git") .args(["clone", TEST_REPO_URL, path]) .status() @@ -303,15 +196,6 @@ mod test { true, ) .await; - Command::new("git") - .args([ - "--no-pager", - "show", - "a2875ac00e6f1dc3eb4ac19712c7a241b5a76e83", - "--format=%b", - ]) - .status() - .unwrap(); eprintln!("files: {files:?}"); assert!(files.is_empty()); set_current_dir(cur_dir).unwrap(); // prep to delete temp_folder diff --git a/cpp-linter/src/rest_client/mod.rs b/cpp-linter/src/rest_client/mod.rs index f2b97d11..fce028eb 100644 --- a/cpp-linter/src/rest_client/mod.rs +++ b/cpp-linter/src/rest_client/mod.rs @@ -142,10 +142,11 @@ impl RestClient { )); } let options = ThreadCommentOptions { - policy: match feedback_inputs.thread_comments { - ThreadComments::Update => CommentPolicy::Update, - ThreadComments::On => CommentPolicy::Anew, - ThreadComments::Off => unreachable!(), + policy: if feedback_inputs.thread_comments == ThreadComments::Update { + CommentPolicy::Update + } else { + // feedback_inputs.thread_comments is not Off and not Update, so it must be just On. + CommentPolicy::Anew }, comment: comment.unwrap_or_default(), kind: if format_checks_failed == 0 && tidy_checks_failed == 0 { @@ -258,10 +259,9 @@ impl RestClient { let title = format!("{}:{}:{}", note.filename, note.line, note.cols); let annotation = FileAnnotation { severity: match note.severity.as_str() { - "note" => AnnotationLevel::Notice, "warning" => AnnotationLevel::Warning, "error" => AnnotationLevel::Error, - _ => AnnotationLevel::Notice, // default to notice if severity is unrecognized + _ => AnnotationLevel::Notice, // default to notice for all else }, path, start_line: None, @@ -417,7 +417,7 @@ mod test { default::Default, env, io::Read, - path::{Path, PathBuf}, + path::PathBuf, sync::{Arc, Mutex}, }; @@ -439,11 +439,7 @@ mod test { // ************************* tests for step-summary and output variables - async fn create_comment( - is_lgtm: bool, - fail_gh_out: bool, - fail_summary: bool, - ) -> (String, String) { + async fn create_comment(is_lgtm: bool) -> (String, String) { let tmp_dir = tempdir().unwrap(); unsafe { // ensure we are mimicking a CI platform @@ -490,27 +486,14 @@ mod test { String::from("file") }, step_summary: true, + file_annotations: false, ..Default::default() }; let mut step_summary_path = NamedTempFile::new_in(tmp_dir.path()).unwrap(); let mut gh_out_path = NamedTempFile::new_in(tmp_dir.path()).unwrap(); unsafe { - env::set_var( - "GITHUB_STEP_SUMMARY", - if fail_summary { - Path::new("not-a-file.txt") - } else { - step_summary_path.path() - }, - ); - env::set_var( - "GITHUB_OUTPUT", - if fail_gh_out { - Path::new("not-a-file.txt") - } else { - gh_out_path.path() - }, - ); + env::set_var("GITHUB_STEP_SUMMARY", step_summary_path.path()); + env::set_var("GITHUB_OUTPUT", gh_out_path.path()); } let clang_versions = ClangVersions { format_version: Some(Version::new(1, 2, 3)), @@ -524,20 +507,16 @@ mod test { step_summary_path .read_to_string(&mut step_summary_content) .unwrap(); - if !fail_summary { - assert!(&step_summary_content.contains(USER_OUTREACH)); - } + assert!(&step_summary_content.contains(USER_OUTREACH)); let mut gh_out_content = String::new(); gh_out_path.read_to_string(&mut gh_out_content).unwrap(); - if !fail_gh_out { - assert!(gh_out_content.starts_with("checks-failed=")); - } + assert!(gh_out_content.starts_with("checks-failed=")); (step_summary_content, gh_out_content) } #[tokio::test] async fn check_comment_concerns() { - let (comment, gh_out) = create_comment(false, false, false).await; + let (comment, gh_out) = create_comment(false).await; assert!(&comment.contains(":warning:\nSome files did not pass the configured checks!\n")); let fmt_pattern = Regex::new(r"format-checks-failed=(\d+)\n").unwrap(); let tidy_pattern = Regex::new(r"tidy-checks-failed=(\d+)\n").unwrap(); @@ -559,7 +538,7 @@ mod test { unsafe { env::set_var("ACTIONS_STEP_DEBUG", "true"); } - let (comment, gh_out) = create_comment(true, false, false).await; + let (comment, gh_out) = create_comment(true).await; assert!(comment.contains(":heavy_check_mark:\nNo problems need attention.")); assert_eq!( gh_out,