1use std::{
7 env,
8 path::Path,
9 sync::{Arc, Mutex},
10};
11
12use anyhow::{anyhow, Result};
14use clap::Parser;
15use log::{set_max_level, LevelFilter};
16
17use crate::{
19 clang_tools::capture_clang_tools_output,
20 cli::{ClangParams, Cli, CliCommand, FeedbackInput, LinesChangedOnly, RequestedVersion},
21 common_fs::FileFilter,
22 logger,
23 rest_api::{github::GithubApiClient, RestApiClient},
24};
25
26const VERSION: &str = env!("CARGO_PKG_VERSION");
27
28pub async fn run_main(args: Vec<String>) -> Result<()> {
48 let cli = Cli::parse_from(args);
49
50 if matches!(cli.commands, Some(CliCommand::Version))
51 || cli.general_options.version == RequestedVersion::NoValue
52 {
53 println!("cpp-linter v{}", VERSION);
54 return Ok(());
55 }
56
57 logger::try_init();
58
59 if cli.source_options.repo_root != "." {
60 env::set_current_dir(Path::new(&cli.source_options.repo_root)).map_err(|e| {
61 anyhow!(
62 "'{}' is inaccessible or does not exist: {e:?}",
63 cli.source_options.repo_root
64 )
65 })?;
66 }
67
68 let rest_api_client = GithubApiClient::new()?;
69 set_max_level(
70 if cli.general_options.verbosity.is_debug() || rest_api_client.debug_enabled {
71 LevelFilter::Debug
72 } else {
73 LevelFilter::Info
74 },
75 );
76 log::info!("Processing event {}", rest_api_client.event_name);
77 let is_pr = rest_api_client.event_name == "pull_request";
78
79 let mut file_filter = FileFilter::new(
80 &cli.source_options.ignore,
81 cli.source_options.extensions.clone(),
82 );
83 file_filter.parse_submodules();
84 if let Some(files) = &cli.not_ignored {
85 file_filter.not_ignored.extend(files.clone());
86 }
87
88 if !file_filter.ignored.is_empty() {
89 log::info!("Ignored:");
90 for pattern in &file_filter.ignored {
91 log::info!(" {pattern}");
92 }
93 }
94 if !file_filter.not_ignored.is_empty() {
95 log::info!("Not Ignored:");
96 for pattern in &file_filter.not_ignored {
97 log::info!(" {pattern}");
98 }
99 }
100
101 rest_api_client.start_log_group(String::from("Get list of specified source files"));
102 let files = if !matches!(cli.source_options.lines_changed_only, LinesChangedOnly::Off)
103 || cli.source_options.files_changed_only
104 {
105 rest_api_client
107 .get_list_of_changed_files(&file_filter, &cli.source_options.lines_changed_only)
108 .await?
109 } else {
110 let mut all_files = file_filter.list_source_files(".")?;
112 if is_pr && (cli.feedback_options.tidy_review || cli.feedback_options.format_review) {
113 let changed_files = rest_api_client
114 .get_list_of_changed_files(&file_filter, &LinesChangedOnly::Off)
115 .await?;
116 for changed_file in changed_files {
117 for file in &mut all_files {
118 if changed_file.name == file.name {
119 file.diff_chunks = changed_file.diff_chunks.clone();
120 file.added_lines = changed_file.added_lines.clone();
121 file.added_ranges = changed_file.added_ranges.clone();
122 }
123 }
124 }
125 }
126 all_files
127 };
128 let mut arc_files = vec![];
129 log::info!("Giving attention to the following files:");
130 for file in files {
131 log::info!(" ./{}", file.name.to_string_lossy().replace('\\', "/"));
132 arc_files.push(Arc::new(Mutex::new(file)));
133 }
134 rest_api_client.end_log_group();
135
136 let mut clang_params = ClangParams::from(&cli);
137 clang_params.format_review &= is_pr;
138 clang_params.tidy_review &= is_pr;
139 let user_inputs = FeedbackInput::from(&cli);
140 let clang_versions = capture_clang_tools_output(
141 &mut arc_files,
142 &cli.general_options.version,
143 &mut clang_params,
144 &rest_api_client,
145 )
146 .await?;
147 rest_api_client.start_log_group(String::from("Posting feedback"));
148 let checks_failed = rest_api_client
149 .post_feedback(&arc_files, user_inputs, clang_versions)
150 .await?;
151 rest_api_client.end_log_group();
152 if env::var("PRE_COMMIT").is_ok_and(|v| v == "1") && checks_failed > 1 {
153 return Err(anyhow!("Some checks did not pass"));
154 }
155 Ok(())
156}
157
158#[cfg(test)]
159mod test {
160 use super::run_main;
161 use std::env;
162
163 #[tokio::test]
164 async fn normal() {
165 env::remove_var("GITHUB_OUTPUT"); let result = run_main(vec![
167 "cpp-linter".to_string(),
168 "-l".to_string(),
169 "false".to_string(),
170 "--repo-root".to_string(),
171 "tests".to_string(),
172 "demo/demo.cpp".to_string(),
173 ])
174 .await;
175 assert!(result.is_ok());
176 }
177
178 #[tokio::test]
179 async fn version_command() {
180 env::remove_var("GITHUB_OUTPUT"); let result = run_main(vec!["cpp-linter".to_string(), "version".to_string()]).await;
182 assert!(result.is_ok());
183 }
184
185 #[tokio::test]
186 async fn force_debug_output() {
187 env::remove_var("GITHUB_OUTPUT"); let result = run_main(vec![
189 "cpp-linter".to_string(),
190 "-l".to_string(),
191 "false".to_string(),
192 "-v".to_string(),
193 "-i=target|benches/libgit2".to_string(),
194 ])
195 .await;
196 assert!(result.is_ok());
197 }
198
199 #[tokio::test]
200 async fn no_version_input() {
201 env::remove_var("GITHUB_OUTPUT"); let result = run_main(vec![
203 "cpp-linter".to_string(),
204 "-l".to_string(),
205 "false".to_string(),
206 "-V".to_string(),
207 ])
208 .await;
209 assert!(result.is_ok());
210 }
211
212 #[tokio::test]
213 async fn pre_commit_env() {
214 env::remove_var("GITHUB_OUTPUT"); env::set_var("PRE_COMMIT", "1");
216 let result = run_main(vec![
217 "cpp-linter".to_string(),
218 "--lines-changed-only".to_string(),
219 "false".to_string(),
220 "--ignore=target|benches/libgit2".to_string(),
221 ])
222 .await;
223 assert!(result.is_err());
224 }
225
226 #[tokio::test]
229 async fn no_analysis() {
230 env::remove_var("GITHUB_OUTPUT"); let result = run_main(vec![
232 "cpp-linter".to_string(),
233 "-l".to_string(),
234 "false".to_string(),
235 "--style".to_string(),
236 String::new(),
237 "--tidy-checks=-*".to_string(),
238 ])
239 .await;
240 assert!(result.is_ok());
241 }
242
243 #[tokio::test]
244 async fn bad_repo_root() {
245 env::remove_var("GITHUB_OUTPUT"); let result = run_main(vec![
247 "cpp-linter".to_string(),
248 "--repo-root".to_string(),
249 "some-non-existent-dir".to_string(),
250 ])
251 .await;
252 assert!(result.is_err());
253 }
254}