Skip to content

Commit 17ae679

Browse files
committed
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
1 parent 000aa75 commit 17ae679

36 files changed

Lines changed: 1234 additions & 7918 deletions

Cargo.lock

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

clang-installer/src/downloader/mod.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,17 @@ async fn download(url: &Url, cache_path: &Path, timeout: u64) -> Result<(), Down
6060
}
6161
let mut tmp_file = tempfile::NamedTempFile::new()?;
6262
let content_len = response.content_length().and_then(NonZero::new);
63-
let mut progress_bar = ProgressBar::new(content_len, "Downloading");
63+
let mut progress_bar = ProgressBar::new(
64+
content_len,
65+
format!(
66+
"Downloading {}",
67+
cache_path
68+
.file_name()
69+
.map(|p| p.to_string_lossy())
70+
.unwrap_or_default()
71+
)
72+
.as_str(),
73+
);
6474
progress_bar.render()?;
6575
while let Some(chunk) = response.chunk().await? {
6676
let chunk_len = chunk.len() as u64;

clang-installer/src/main.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,6 @@ pub struct CliOptions {
132132
async fn main() -> Result<()> {
133133
logging::initialize_logger();
134134
let options = CliOptions::parse();
135-
log::debug!("{:?}", options);
136135

137136
let tool = options
138137
.tool

clang-installer/src/progress_bar.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ impl ProgressBar {
5959
steps: 0,
6060
stdout_handle,
6161
is_interactive,
62-
prompt: prompt.to_string(),
62+
prompt: prompt.trim().to_string(),
6363
}
6464
}
6565

clang-installer/src/tool.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ pub enum GetClangVersionError {
4444
RegexCompile(#[from] regex::Error),
4545

4646
/// Failed to parse the version number from the output of `clang-tool --version`.
47-
#[error("Failed to parse the version number from the `--version` output")]
48-
VersionParse,
47+
#[error("Failed to parse the version number from the `--version` output: {0}")]
48+
VersionParse(String),
4949

5050
/// Failed to parse the version number from the output of `clang-tool --version` into a [`semver::Version`].
5151
#[error("Failed to parse the version number from the `--version` output: {0}")]
@@ -149,12 +149,13 @@ impl ClangTool {
149149
.map_err(|e| GetClangVersionError::Command(path.to_path_buf(), e))?;
150150
let stdout = String::from_utf8_lossy(&output.stdout);
151151
let version_pattern = Regex::new(r"(?i)version[^\d]*([\d.]+)")?;
152-
let captures = version_pattern
153-
.captures(&stdout)
154-
.ok_or(GetClangVersionError::VersionParse)?;
155-
let result = captures.get(1).ok_or(GetClangVersionError::VersionParse)?;
156-
let version = Version::parse(result.as_str())?;
157-
Ok(version)
152+
if let Some(captures) = version_pattern.captures(&stdout)
153+
&& let Some(result) = captures.get(1)
154+
{
155+
let version = Version::parse(result.as_str())?;
156+
return Ok(version);
157+
}
158+
Err(GetClangVersionError::VersionParse(stdout.to_string()))
158159
}
159160

160161
pub fn symlink_bin(

clang-installer/src/version.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ pub enum GetToolError {
5656
ExecutablePathNoParent,
5757

5858
/// Failed to capture the clang version from `--version` output.
59-
#[error("Failed to capture the clang version from `--version` output: {0}")]
59+
#[error(transparent)]
6060
GetClangVersion(#[from] GetClangVersionError),
6161

6262
/// Failed to get the clang executable path.
@@ -299,7 +299,7 @@ mod tests {
299299
// for this test we should use the oldest supported clang version
300300
// because that would be most likely to require downloading.
301301
let version_req =
302-
VersionReq::parse(option_env!("MIN_CLANG_TOOLS_VERSION").unwrap_or("11")).unwrap();
302+
VersionReq::parse(option_env!("MIN_CLANG_TOOLS_VERSION").unwrap_or("16")).unwrap();
303303
let downloaded_clang = RequestedVersion::Requirement(version_req.clone())
304304
.eval_tool(&tool, false, Some(&PathBuf::from(tmp_cache_dir.path())))
305305
.await

cpp-linter/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ license.workspace = true
1515

1616
[dependencies]
1717
anyhow = { workspace = true }
18+
async-trait = "0.1.89"
1819
chrono = "0.4.44"
1920
clang-installer = { path = "../clang-installer", version = "0.1.0" }
2021
clap = { workspace = true, optional = true }
2122
colored = { workspace = true, optional = true }
2223
fast-glob = "1.0.1"
2324
futures = "0.3.32"
25+
git-bot-feedback = { version = "0.5.2", features = ["file-changes"] }
2426
git2 = "0.20.4"
2527
log = { workspace = true }
2628
quick-xml = { version = "0.39.2", features = ["serialize"] }

cpp-linter/src/clang_tools/clang_format.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,10 @@ pub fn summarize_style(style: &str) -> String {
6868
}
6969

7070
/// Get a total count of clang-format advice from the given list of [FileObj]s.
71-
pub fn tally_format_advice(files: &[Arc<Mutex<FileObj>>]) -> Result<u64> {
71+
pub fn tally_format_advice(files: &[Arc<Mutex<FileObj>>]) -> Result<u64, String> {
7272
let mut total = 0;
7373
for file in files {
74-
let file = file
75-
.lock()
76-
.map_err(|_| anyhow!("Failed to acquire lock on mutex for a source file"))?;
74+
let file = file.lock().map_err(|e| e.to_string())?;
7775
if let Some(advice) = &file.format_advice
7876
&& !advice.replacements.is_empty()
7977
{

cpp-linter/src/clang_tools/clang_tidy.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,12 +247,10 @@ fn parse_tidy_output(
247247
}
248248

249249
/// Get a total count of clang-tidy advice from the given list of [FileObj]s.
250-
pub fn tally_tidy_advice(files: &[Arc<Mutex<FileObj>>]) -> Result<u64> {
250+
pub fn tally_tidy_advice(files: &[Arc<Mutex<FileObj>>]) -> Result<u64, String> {
251251
let mut total = 0;
252252
for file in files {
253-
let file = file
254-
.lock()
255-
.map_err(|_| anyhow!("Failed to acquire lock on mutex for a source file"))?;
253+
let file = file.lock().map_err(|e| e.to_string())?;
256254
if let Some(advice) = &file.tidy_advice {
257255
for tidy_note in &advice.notes {
258256
let file_path = PathBuf::from(&tidy_note.filename);

cpp-linter/src/clang_tools/mod.rs

Lines changed: 64 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ use std::{
1111
// non-std crates
1212
use anyhow::{Context, Result, anyhow};
1313
use clang_installer::{ClangTool, RequestedVersion};
14+
use git_bot_feedback::ReviewComment;
1415
use git2::{DiffOptions, Patch};
1516
use semver::Version;
1617
use tokio::task::JoinSet;
1718

1819
// project-specific modules/crates
1920
use super::common_fs::FileObj;
21+
use crate::error::SuggestionError;
2022
use crate::{
2123
cli::ClangParams,
22-
rest_api::{COMMENT_MARKER, RestApiClient, USER_OUTREACH},
24+
rest_client::{RestClient, USER_OUTREACH},
2325
};
2426
pub mod clang_format;
2527
use clang_format::run_clang_format;
@@ -46,7 +48,7 @@ fn analyze_single_file(
4648
if clang_params
4749
.format_filter
4850
.as_ref()
49-
.is_some_and(|f| f.is_source_or_ignored(file.name.as_path()))
51+
.is_some_and(|f| f.is_qualified(file.name.as_path()))
5052
|| clang_params.format_filter.is_none()
5153
{
5254
let format_result = run_clang_format(&mut file, &clang_params)?;
@@ -65,7 +67,7 @@ fn analyze_single_file(
6567
if clang_params
6668
.tidy_filter
6769
.as_ref()
68-
.is_some_and(|f| f.is_source_or_ignored(file.name.as_path()))
70+
.is_some_and(|f| f.is_qualified(file.name.as_path()))
6971
|| clang_params.tidy_filter.is_none()
7072
{
7173
let tidy_result = run_clang_tidy(&mut file, &clang_params)?;
@@ -101,7 +103,7 @@ pub async fn capture_clang_tools_output(
101103
files: &[Arc<Mutex<FileObj>>],
102104
version: &RequestedVersion,
103105
mut clang_params: ClangParams,
104-
rest_api_client: &impl RestApiClient,
106+
rest_api_client: &RestClient,
105107
) -> Result<ClangVersions> {
106108
let mut clang_versions = ClangVersions::default();
107109
// 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(
148150
// This includes any `spawn()` error and any `analyze_single_file()` error.
149151
// Any unresolved tasks are aborted and dropped when an error is returned here.
150152
let (file_name, logs) = output??;
151-
rest_api_client.start_log_group(format!("Analyzing {}", file_name.to_string_lossy()));
153+
let log_group_name = format!("Analyzing {}", file_name.to_string_lossy());
154+
rest_api_client.start_log_group(&log_group_name);
152155
for (level, msg) in logs {
153156
log::log!(level, "{}", msg);
154157
}
155-
rest_api_client.end_log_group();
158+
rest_api_client.end_log_group(&log_group_name);
156159
}
157160
Ok(clang_versions)
158161
}
@@ -169,6 +172,17 @@ pub struct Suggestion {
169172
pub path: String,
170173
}
171174

175+
impl Suggestion {
176+
pub(crate) fn as_review_comment(&self) -> ReviewComment {
177+
ReviewComment {
178+
line_start: Some(self.line_start),
179+
line_end: self.line_end,
180+
comment: self.suggestion.clone(),
181+
path: self.path.clone(),
182+
}
183+
}
184+
}
185+
172186
/// A struct to describe the Pull Request review suggestions.
173187
#[derive(Default)]
174188
pub struct ReviewComments {
@@ -189,8 +203,12 @@ pub struct ReviewComments {
189203
}
190204

191205
impl ReviewComments {
192-
pub fn summarize(&self, clang_versions: &ClangVersions) -> String {
193-
let mut body = format!("{COMMENT_MARKER}## Cpp-linter Review\n");
206+
pub fn summarize(
207+
&self,
208+
clang_versions: &ClangVersions,
209+
comments: &Vec<ReviewComment>,
210+
) -> String {
211+
let mut body = String::from("## Cpp-linter Review\n");
194212
for t in 0_usize..=1 {
195213
let mut total = 0;
196214
let (tool_name, tool_version) = if t == 0 {
@@ -209,9 +227,9 @@ impl ReviewComments {
209227
if let Some(ver_str) = tool_version {
210228
body.push_str(format!("\n### Used {tool_name} v{ver_str}\n").as_str());
211229
}
212-
for comment in &self.comments {
230+
for comment in comments {
213231
if comment
214-
.suggestion
232+
.comment
215233
.contains(format!("### {tool_name}").as_str())
216234
{
217235
total += 1;
@@ -266,24 +284,17 @@ pub fn make_patch<'buffer>(
266284
path: &Path,
267285
patched: &'buffer [u8],
268286
original_content: &'buffer [u8],
269-
) -> Result<Patch<'buffer>> {
287+
) -> Result<Patch<'buffer>, git2::Error> {
270288
let mut diff_opts = &mut DiffOptions::new();
271289
diff_opts = diff_opts.indent_heuristic(true);
272290
diff_opts = diff_opts.context_lines(0);
273-
let patch = Patch::from_buffers(
291+
Patch::from_buffers(
274292
original_content,
275293
Some(path),
276294
patched,
277295
Some(path),
278296
Some(diff_opts),
279297
)
280-
.with_context(|| {
281-
format!(
282-
"Failed to create patch for file {}.",
283-
path.to_string_lossy()
284-
)
285-
})?;
286-
Ok(patch)
287298
}
288299

289300
/// A trait for generating suggestions from a [`FileObj`]'s advice's generated `patched` buffer.
@@ -301,7 +312,7 @@ pub trait MakeSuggestions {
301312
file_obj: &FileObj,
302313
patch: &mut Patch,
303314
summary_only: bool,
304-
) -> Result<()> {
315+
) -> Result<(), SuggestionError> {
305316
let is_tidy_tool = (&self.get_tool_name() == "clang-tidy") as usize;
306317
let hunks_total = patch.num_hunks();
307318
let mut hunks_in_patch = 0u32;
@@ -313,21 +324,32 @@ pub trait MakeSuggestions {
313324
.to_owned();
314325
let patch_buf = &patch
315326
.to_buf()
316-
.with_context(|| "Failed to convert patch to byte array")?
327+
.map_err(|e| SuggestionError::PatchIntoBytesFailed {
328+
file_name: file_name.clone(),
329+
source: e,
330+
})?
317331
.to_vec();
318332
review_comments.full_patch[is_tidy_tool].push_str(
319333
String::from_utf8(patch_buf.to_owned())
320-
.with_context(|| format!("Failed to convert patch to string: {file_name}"))?
334+
.map_err(|e| SuggestionError::PatchIntoStringFailed {
335+
file_name: file_name.clone(),
336+
source: e,
337+
})?
321338
.as_str(),
322339
);
323340
if summary_only {
324341
review_comments.tool_total[is_tidy_tool].get_or_insert(0);
325342
return Ok(());
326343
}
327344
for hunk_id in 0..hunks_total {
328-
let (hunk, line_count) = patch.hunk(hunk_id).with_context(|| {
329-
format!("Failed to get hunk {hunk_id} from patch for {file_name}")
330-
})?;
345+
let (hunk, line_count) =
346+
patch
347+
.hunk(hunk_id)
348+
.map_err(|e| SuggestionError::GetHunkFailed {
349+
hunk_id,
350+
file_name: file_name.clone(),
351+
source: e,
352+
})?;
331353
hunks_in_patch += 1;
332354
let hunk_range = file_obj.is_hunk_in_diff(&hunk);
333355
match hunk_range {
@@ -337,11 +359,23 @@ pub trait MakeSuggestions {
337359
let suggestion_help = self.get_suggestion_help(start_line, end_line);
338360
let mut removed = vec![];
339361
for line_index in 0..line_count {
340-
let diff_line = patch
341-
.line_in_hunk(hunk_id, line_index)
342-
.with_context(|| format!("Failed to get line {line_index} in a hunk {hunk_id} of patch for {file_name}"))?;
343-
let line = String::from_utf8(diff_line.content().to_owned())
344-
.with_context(|| format!("Failed to convert line {line_index} buffer to string in hunk {hunk_id} of patch for {file_name}"))?;
362+
let diff_line = patch.line_in_hunk(hunk_id, line_index).map_err(|e| {
363+
SuggestionError::GetHunkLineFailed {
364+
line_index,
365+
hunk_id,
366+
file_name: file_name.clone(),
367+
source: e,
368+
}
369+
})?;
370+
let line =
371+
String::from_utf8(diff_line.content().to_owned()).map_err(|e| {
372+
SuggestionError::HunkLineIntoStringFailed {
373+
line_index,
374+
hunk_id,
375+
file_name: file_name.clone(),
376+
source: e,
377+
}
378+
})?;
345379
if ['+', ' '].contains(&diff_line.origin()) {
346380
suggestion.push_str(line.as_str());
347381
} else {

0 commit comments

Comments
 (0)