Skip to content

Commit b213c8a

Browse files
authored
feat: make diff without libgit2 (#344)
resolves #303 This uses a pure-rust library called [imara-diff], which has been forked by the [gitoxide] project as a library named [gix-imara-diff]. I'm using the fork because the [imara-diff] lib has not been updated as recently as [gix-imara-diff]. Also, the [gix-imara-diff] is what is being used in the [gitoxide] project, so I think it is better maintained with a `git`-based focus in mind. It boasts the same unified diff algorithms as used by `git` CLI. In fact, the [gitoxide] project is intended to be a rust implementation of `git` CLI and provides an API comparable to libgit2 (but with much more separation of concerns). Creating and traversing a diff was the remaining use of libgit2 in our production code. Thus, libgit2 has been removed from our dependencies. However, the benchmark still uses libgit2 as the sample source files. Additionally, libgit2 had some memory safety concerns and because it is written in C, so [gix-imara-diff] should be safer to use with no unexpected panics at runtime. [imara-diff]: https://github.com/pascalkuthe/imara-diff [gix-imara-diff]: https://github.com/GitoxideLabs/gitoxide/tree/main/gix-imara-diff [gitoxide]: https://github.com/GitoxideLabs/gitoxide
1 parent cdc871a commit b213c8a

6 files changed

Lines changed: 84 additions & 196 deletions

File tree

Cargo.lock

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

cpp-linter/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ clap = { workspace = true, optional = true }
2222
colored = { workspace = true, optional = true }
2323
fast-glob = "1.0.1"
2424
futures = "0.3.32"
25-
git2 = { version = "0.21.0" }
25+
gix-imara-diff = { version = "0.2.2", features = ["unified_diff"] }
2626
log = { workspace = true }
2727
quick-xml = { version = "0.40.1", features = ["serialize"] }
2828
regex = { workspace = true }

cpp-linter/src/clang_tools/mod.rs

Lines changed: 37 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@
44
55
use std::{
66
fs,
7-
path::{Path, PathBuf},
7+
path::PathBuf,
88
sync::{Arc, Mutex},
99
};
1010

1111
// non-std crates
1212
use clang_tools_manager::{ClangTool, RequestedVersion};
1313
use git_bot_feedback::ReviewComment;
14-
use git2::{DiffOptions, Patch};
14+
use gix_imara_diff::{BasicLineDiffPrinter, Diff, InternedInput, UnifiedDiffConfig};
1515
use semver::Version;
1616
use tokio::task::JoinSet;
1717

1818
// project-specific modules/crates
1919
use super::common_fs::FileObj;
20-
use crate::error::{ClangCaptureError, ClangTaskError, SuggestionError};
20+
use crate::error::{ClangCaptureError, ClangTaskError};
2121
use crate::{
2222
cli::ClangParams,
2323
rest_client::{RestClient, USER_OUTREACH},
@@ -291,20 +291,13 @@ impl ReviewComments {
291291
}
292292

293293
pub fn make_patch<'buffer>(
294-
path: &Path,
295-
patched: &'buffer [u8],
296-
original_content: &'buffer [u8],
297-
) -> Result<Patch<'buffer>, git2::Error> {
298-
let mut diff_opts = &mut DiffOptions::new();
299-
diff_opts = diff_opts.indent_heuristic(true);
300-
diff_opts = diff_opts.context_lines(0);
301-
Patch::from_buffers(
302-
original_content,
303-
Some(path),
304-
patched,
305-
Some(path),
306-
Some(diff_opts),
307-
)
294+
patched: &'buffer str,
295+
original_content: &'buffer str,
296+
) -> (Diff, InternedInput<&'buffer str>) {
297+
let input = InternedInput::new(original_content, patched);
298+
let mut diff = Diff::compute(gix_imara_diff::Algorithm::Histogram, &input);
299+
diff.postprocess_lines(&input);
300+
(diff, input)
308301
}
309302

310303
/// A trait for generating suggestions from a [`FileObj`]'s advice's generated `patched` buffer.
@@ -320,96 +313,58 @@ pub trait MakeSuggestions {
320313
&self,
321314
review_comments: &mut ReviewComments,
322315
file_obj: &FileObj,
323-
patch: &mut Patch,
316+
diff: &Diff,
317+
input: &InternedInput<&str>,
324318
summary_only: bool,
325-
) -> Result<(), SuggestionError> {
319+
) {
326320
let is_tidy_tool = (&self.get_tool_name() == "clang-tidy") as usize;
327-
let hunks_total = patch.num_hunks();
328-
let mut hunks_in_patch = 0u32;
329321
let file_name = file_obj
330322
.name
331323
.to_string_lossy()
332324
.replace("\\", "/")
333325
.trim_start_matches("./")
334326
.to_owned();
335-
let patch_buf = &patch
336-
.to_buf()
337-
.map_err(|e| SuggestionError::PatchIntoBytesFailed {
338-
file_name: file_name.clone(),
339-
source: e,
340-
})?
341-
.to_vec();
342-
review_comments.full_patch[is_tidy_tool].push_str(
343-
String::from_utf8(patch_buf.to_owned())
344-
.map_err(|e| SuggestionError::PatchIntoStringFailed {
345-
file_name: file_name.clone(),
346-
source: e,
347-
})?
348-
.as_str(),
349-
);
327+
let mut config = UnifiedDiffConfig::default();
328+
config.context_len(0);
329+
let printer = BasicLineDiffPrinter(&input.interner);
330+
let unified_diff = diff.unified_diff(&printer, config, input).to_string();
331+
if !unified_diff.is_empty() {
332+
let patch_buf = format!("--- a/{file_name}\n+++ b/{file_name}\n{unified_diff}");
333+
review_comments.full_patch[is_tidy_tool].push_str(patch_buf.as_str());
334+
}
350335
if summary_only {
351336
review_comments.tool_total[is_tidy_tool].get_or_insert(0);
352-
return Ok(());
337+
return;
353338
}
354-
for hunk_id in 0..hunks_total {
355-
let (hunk, line_count) =
356-
patch
357-
.hunk(hunk_id)
358-
.map_err(|e| SuggestionError::GetHunkFailed {
359-
hunk_id,
360-
file_name: file_name.clone(),
361-
source: e,
362-
})?;
339+
let mut hunks_in_patch = 0u32;
340+
for hunk in diff.hunks() {
363341
hunks_in_patch += 1;
364342
let hunk_range = file_obj.is_hunk_in_diff(&hunk);
365343
match hunk_range {
366344
None => continue,
367345
Some((start_line, end_line)) => {
368346
let mut suggestion = String::new();
369347
let suggestion_help = self.get_suggestion_help(start_line, end_line);
370-
let mut removed = vec![];
371-
for line_index in 0..line_count {
372-
let diff_line = patch.line_in_hunk(hunk_id, line_index).map_err(|e| {
373-
SuggestionError::GetHunkLineFailed {
374-
line_index,
375-
hunk_id,
376-
file_name: file_name.clone(),
377-
source: e,
378-
}
379-
})?;
380-
let line =
381-
String::from_utf8(diff_line.content().to_owned()).map_err(|e| {
382-
SuggestionError::HunkLineIntoStringFailed {
383-
line_index,
384-
hunk_id,
385-
file_name: file_name.clone(),
386-
source: e,
387-
}
388-
})?;
389-
if ['+', ' '].contains(&diff_line.origin()) {
390-
suggestion.push_str(line.as_str());
391-
} else {
392-
removed.push(
393-
diff_line
394-
.old_lineno()
395-
.expect("Removed line should have a line number"),
396-
);
397-
}
398-
}
399-
if suggestion.is_empty() && !removed.is_empty() {
348+
if hunk.is_pure_removal() {
400349
suggestion.push_str(
401350
format!(
402351
"Please remove the line(s)\n- {}",
403-
removed
404-
.iter()
352+
hunk.before
405353
.map(|l| l.to_string())
406354
.collect::<Vec<String>>()
407355
.join("\n- ")
408356
)
409357
.as_str(),
410-
)
358+
);
411359
} else {
412-
suggestion = format!("```suggestion\n{suggestion}```");
360+
suggestion.push_str("```suggestion\n");
361+
for token in
362+
&input.after[hunk.after.start as usize..hunk.after.end as usize]
363+
{
364+
let line = &input.interner[*token];
365+
suggestion.push_str(line);
366+
}
367+
suggestion.push_str("```\n");
413368
}
414369
let comment = Suggestion {
415370
line_start: start_line,
@@ -423,8 +378,7 @@ pub trait MakeSuggestions {
423378
}
424379
}
425380
}
426-
review_comments.tool_total[is_tidy_tool] =
427-
Some(review_comments.tool_total[is_tidy_tool].unwrap_or_default() + hunks_in_patch);
428-
Ok(())
381+
let tool_total = review_comments.tool_total[is_tidy_tool].get_or_insert(0);
382+
*tool_total += hunks_in_patch;
429383
}
430384
}

0 commit comments

Comments
 (0)