#![deny(clippy::unwrap_used)]
#![cfg(feature = "bin")]
use std::{
env,
path::{Path, PathBuf},
sync::{Arc, Mutex},
};
use anyhow::{Result, anyhow};
use clang_tools_manager::RequestedVersion;
use clap::Parser;
use log::{LevelFilter, set_max_level};
use crate::{
clang_tools::capture_clang_tools_output,
cli::{ClangParams, Cli, CliCommand, FeedbackInput, LinesChangedOnly},
common_fs::FileObj,
logger,
rest_client::RestClient,
};
use git_bot_feedback::FileFilter;
const VERSION: &str = env!("CARGO_PKG_VERSION");
pub async fn run_main(args: Vec<String>) -> Result<()> {
let cli = Cli::parse_from(args);
if matches!(cli.commands, Some(CliCommand::Version))
|| cli.general_options.version == RequestedVersion::NoValue
{
println!("cpp-linter v{}", VERSION);
return Ok(());
}
logger::try_init();
if cli.source_options.repo_root != "." {
env::set_current_dir(Path::new(&cli.source_options.repo_root)).map_err(|e| {
anyhow!(
"'{}' is inaccessible or does not exist: {e:?}",
cli.source_options.repo_root
)
})?;
}
let mut rest_api_client = RestClient::new()?;
set_max_level(
if cli.general_options.verbosity.is_debug()
{
LevelFilter::Debug
} else {
LevelFilter::Info
},
);
let is_pr = rest_api_client.is_pr();
let mut file_filter = FileFilter::new(
&cli.source_options
.ignore
.iter()
.map(|s| s.as_str())
.collect::<Vec<&str>>(),
&cli.source_options
.extensions
.iter()
.map(|s| s.as_str())
.collect::<Vec<&str>>(),
None,
);
file_filter.parse_submodules();
if let Some(files) = &cli.not_ignored {
file_filter.not_ignored.extend(files.clone());
}
if !file_filter.ignored.is_empty() {
log::info!("Ignored:");
for pattern in &file_filter.ignored {
log::info!(" {pattern}");
}
}
if !file_filter.not_ignored.is_empty() {
log::info!("Not Ignored:");
for pattern in &file_filter.not_ignored {
log::info!(" {pattern}");
}
}
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
{
rest_api_client
.get_list_of_changed_files(
&file_filter,
&cli.source_options.lines_changed_only.clone().into(),
&cli.source_options.diff_base,
cli.source_options.ignore_index,
)
.await?
} else {
let mut all_files: Vec<FileObj> = 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.into(),
&cli.source_options.diff_base,
cli.source_options.ignore_index,
)
.await?;
for changed_file in changed_files {
for file in &mut all_files {
if changed_file.name == file.name {
file.diff_chunks = changed_file.diff_chunks.clone();
file.added_lines = changed_file.added_lines.clone();
file.added_ranges = changed_file.added_ranges.clone();
}
}
}
}
all_files
};
let mut arc_files = vec![];
log::info!("Giving attention to the following files:");
for file in files {
log::info!(" ./{}", file.name.to_string_lossy().replace('\\', "/"));
arc_files.push(Arc::new(Mutex::new(file)));
}
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;
clang_params.tidy_review &= is_pr;
let user_inputs = FeedbackInput::from(&cli);
let clang_versions = capture_clang_tools_output(
&arc_files,
&cli.general_options.version,
clang_params,
&rest_api_client,
)
.await?;
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("Posting feedback");
if env::var("PRE_COMMIT").is_ok_and(|v| v == "1") && checks_failed > 1 {
return Err(anyhow!("Some checks did not pass"));
}
Ok(())
}
#[cfg(test)]
mod test {
#![allow(clippy::unwrap_used)]
use super::run_main;
use std::env;
fn setup_tmp_gh_out_path() -> tempfile::NamedTempFile {
let gh_out_path = tempfile::NamedTempFile::new().unwrap();
unsafe {
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(),
"false".to_string(),
"--repo-root".to_string(),
"tests".to_string(),
"demo/demo.cpp".to_string(),
])
.await
.unwrap();
drop(tmp_gh_out);
}
#[tokio::test]
async fn version_command() {
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() {
let tmp_gh_out = setup_tmp_gh_out_path();
run_main(vec![
"cpp-linter".to_string(),
"-l".to_string(),
"false".to_string(),
"-v".to_string(),
"-i=target|benches/libgit2".to_string(),
])
.await
.unwrap();
drop(tmp_gh_out);
}
#[tokio::test]
async fn no_version_input() {
let tmp_gh_out = setup_tmp_gh_out_path();
run_main(vec![
"cpp-linter".to_string(),
"-l".to_string(),
"false".to_string(),
"-V".to_string(),
])
.await
.unwrap();
drop(tmp_gh_out);
}
#[tokio::test]
async fn pre_commit_env() {
let tmp_gh_out = setup_tmp_gh_out_path();
unsafe {
env::set_var("PRE_COMMIT", "1");
}
run_main(vec![
"cpp-linter".to_string(),
"--lines-changed-only".to_string(),
"false".to_string(),
"--ignore=target|benches/libgit2".to_string(),
])
.await
.unwrap_err();
drop(tmp_gh_out);
}
#[tokio::test]
async fn no_analysis() {
let tmp_gh_out = setup_tmp_gh_out_path();
run_main(vec![
"cpp-linter".to_string(),
"-l".to_string(),
"false".to_string(),
"--style".to_string(),
String::new(),
"--tidy-checks=-*".to_string(),
])
.await
.unwrap();
drop(tmp_gh_out);
}
#[tokio::test]
async fn bad_repo_root() {
let tmp_gh_out = setup_tmp_gh_out_path();
run_main(vec![
"cpp-linter".to_string(),
"--repo-root".to_string(),
"some-non-existent-dir".to_string(),
])
.await
.unwrap_err();
drop(tmp_gh_out);
}
}