Skip to content

Commit dd5f0a4

Browse files
authored
Merge pull request #2041 from cruessler/add-blame-extraction
Add `it blame-copy-royal`
2 parents 5cf6d05 + 554ce13 commit dd5f0a4

File tree

15 files changed

+540
-25
lines changed

15 files changed

+540
-25
lines changed

gix-attributes/tests/parse/mod.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -322,15 +322,15 @@ fn trailing_whitespace_in_attributes_is_ignored() {
322322

323323
type ExpandedAttribute<'a> = (parse::Kind, Vec<(BString, gix_attributes::StateRef<'a>)>, usize);
324324

325-
fn set(attr: &str) -> (BString, StateRef) {
325+
fn set(attr: &str) -> (BString, StateRef<'_>) {
326326
(attr.into(), StateRef::Set)
327327
}
328328

329-
fn unset(attr: &str) -> (BString, StateRef) {
329+
fn unset(attr: &str) -> (BString, StateRef<'_>) {
330330
(attr.into(), StateRef::Unset)
331331
}
332332

333-
fn unspecified(attr: &str) -> (BString, StateRef) {
333+
fn unspecified(attr: &str) -> (BString, StateRef<'_>) {
334334
(attr.into(), StateRef::Unspecified)
335335
}
336336

@@ -350,36 +350,36 @@ fn pattern(name: &str, flags: gix_glob::pattern::Mode, first_wildcard_pos: Optio
350350
})
351351
}
352352

353-
fn try_line(input: &str) -> Result<ExpandedAttribute, parse::Error> {
353+
fn try_line(input: &str) -> Result<ExpandedAttribute<'_>, parse::Error> {
354354
let mut lines = gix_attributes::parse(input.as_bytes());
355355
let res = expand(lines.next().unwrap())?;
356356
assert!(lines.next().is_none(), "expected only one line");
357357
Ok(res)
358358
}
359359

360-
fn line(input: &str) -> ExpandedAttribute {
360+
fn line(input: &str) -> ExpandedAttribute<'_> {
361361
try_line(input).unwrap()
362362
}
363363

364-
fn byte_line(input: &[u8]) -> ExpandedAttribute {
364+
fn byte_line(input: &[u8]) -> ExpandedAttribute<'_> {
365365
try_byte_line(input).unwrap()
366366
}
367367

368-
fn try_byte_line(input: &[u8]) -> Result<ExpandedAttribute, parse::Error> {
368+
fn try_byte_line(input: &[u8]) -> Result<ExpandedAttribute<'_>, parse::Error> {
369369
let mut lines = gix_attributes::parse(input);
370370
let res = expand(lines.next().unwrap())?;
371371
assert!(lines.next().is_none(), "expected only one line");
372372
Ok(res)
373373
}
374374

375-
fn lenient_lines(input: &str) -> Vec<ExpandedAttribute> {
375+
fn lenient_lines(input: &str) -> Vec<ExpandedAttribute<'_>> {
376376
gix_attributes::parse(input.as_bytes())
377377
.map(expand)
378378
.filter_map(Result::ok)
379379
.collect()
380380
}
381381

382-
fn try_lines(input: &str) -> Result<Vec<ExpandedAttribute>, parse::Error> {
382+
fn try_lines(input: &str) -> Result<Vec<ExpandedAttribute<'_>>, parse::Error> {
383383
gix_attributes::parse(input.as_bytes()).map(expand).collect()
384384
}
385385

gix-blame/src/file/function.rs

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use gix_traverse::commit::find as find_commit;
1010
use smallvec::SmallVec;
1111

1212
use super::{process_changes, Change, UnblamedHunk};
13-
use crate::{BlameEntry, Error, Options, Outcome, Statistics};
13+
use crate::{types::BlamePathEntry, BlameEntry, Error, Options, Outcome, Statistics};
1414

1515
/// Produce a list of consecutive [`BlameEntry`] instances to indicate in which commits the ranges of the file
1616
/// at `suspect:<file_path>` originated in.
@@ -115,6 +115,12 @@ pub fn file(
115115
let mut out = Vec::new();
116116
let mut diff_state = gix_diff::tree::State::default();
117117
let mut previous_entry: Option<(ObjectId, ObjectId)> = None;
118+
let mut blame_path = if options.debug_track_path {
119+
Some(Vec::new())
120+
} else {
121+
None
122+
};
123+
118124
'outer: while let Some(suspect) = queue.pop_value() {
119125
stats.commits_traversed += 1;
120126
if hunks_to_blame.is_empty() {
@@ -156,6 +162,23 @@ pub fn file(
156162
// true here. We could perhaps use diff-tree-to-tree to compare `suspect` against
157163
// an empty tree to validate this assumption.
158164
if unblamed_to_out_is_done(&mut hunks_to_blame, &mut out, suspect) {
165+
if let Some(ref mut blame_path) = blame_path {
166+
let entry = previous_entry
167+
.take()
168+
.filter(|(id, _)| *id == suspect)
169+
.map(|(_, entry)| entry);
170+
171+
let blame_path_entry = BlamePathEntry {
172+
source_file_path: current_file_path.clone(),
173+
previous_source_file_path: None,
174+
commit_id: suspect,
175+
blob_id: entry.unwrap_or(ObjectId::null(gix_hash::Kind::Sha1)),
176+
previous_blob_id: ObjectId::null(gix_hash::Kind::Sha1),
177+
parent_index: 0,
178+
};
179+
blame_path.push(blame_path_entry);
180+
}
181+
159182
break 'outer;
160183
}
161184
}
@@ -241,13 +264,13 @@ pub fn file(
241264
}
242265

243266
let more_than_one_parent = parent_ids.len() > 1;
244-
for (parent_id, parent_commit_time) in parent_ids {
245-
queue.insert(parent_commit_time, parent_id);
267+
for (index, (parent_id, parent_commit_time)) in parent_ids.iter().enumerate() {
268+
queue.insert(*parent_commit_time, *parent_id);
246269
let changes_for_file_path = tree_diff_at_file_path(
247270
&odb,
248271
current_file_path.as_ref(),
249272
suspect,
250-
parent_id,
273+
*parent_id,
251274
cache.as_ref(),
252275
&mut stats,
253276
&mut diff_state,
@@ -262,21 +285,33 @@ pub fn file(
262285
// None of the changes affected the file we’re currently blaming.
263286
// Copy blame to parent.
264287
for unblamed_hunk in &mut hunks_to_blame {
265-
unblamed_hunk.clone_blame(suspect, parent_id);
288+
unblamed_hunk.clone_blame(suspect, *parent_id);
266289
}
267290
} else {
268-
pass_blame_from_to(suspect, parent_id, &mut hunks_to_blame);
291+
pass_blame_from_to(suspect, *parent_id, &mut hunks_to_blame);
269292
}
270293
continue;
271294
};
272295

273296
match modification {
274-
TreeDiffChange::Addition => {
297+
TreeDiffChange::Addition { id } => {
275298
if more_than_one_parent {
276299
// Do nothing under the assumption that this always (or almost always)
277300
// implies that the file comes from a different parent, compared to which
278301
// it was modified, not added.
279302
} else if unblamed_to_out_is_done(&mut hunks_to_blame, &mut out, suspect) {
303+
if let Some(ref mut blame_path) = blame_path {
304+
let blame_path_entry = BlamePathEntry {
305+
source_file_path: current_file_path.clone(),
306+
previous_source_file_path: None,
307+
commit_id: suspect,
308+
blob_id: id,
309+
previous_blob_id: ObjectId::null(gix_hash::Kind::Sha1),
310+
parent_index: index,
311+
};
312+
blame_path.push(blame_path_entry);
313+
}
314+
280315
break 'outer;
281316
}
282317
}
@@ -294,7 +329,22 @@ pub fn file(
294329
options.diff_algorithm,
295330
&mut stats,
296331
)?;
297-
hunks_to_blame = process_changes(hunks_to_blame, changes, suspect, parent_id);
332+
hunks_to_blame = process_changes(hunks_to_blame, changes.clone(), suspect, *parent_id);
333+
if let Some(ref mut blame_path) = blame_path {
334+
let has_blame_been_passed = hunks_to_blame.iter().any(|hunk| hunk.has_suspect(parent_id));
335+
336+
if has_blame_been_passed {
337+
let blame_path_entry = BlamePathEntry {
338+
source_file_path: current_file_path.clone(),
339+
previous_source_file_path: Some(current_file_path.clone()),
340+
commit_id: suspect,
341+
blob_id: id,
342+
previous_blob_id: previous_id,
343+
parent_index: index,
344+
};
345+
blame_path.push(blame_path_entry);
346+
}
347+
}
298348
}
299349
TreeDiffChange::Rewrite {
300350
source_location,
@@ -311,11 +361,29 @@ pub fn file(
311361
options.diff_algorithm,
312362
&mut stats,
313363
)?;
314-
hunks_to_blame = process_changes(hunks_to_blame, changes, suspect, parent_id);
364+
hunks_to_blame = process_changes(hunks_to_blame, changes, suspect, *parent_id);
365+
366+
let mut has_blame_been_passed = false;
315367

316368
for hunk in hunks_to_blame.iter_mut() {
317-
if hunk.has_suspect(&parent_id) {
369+
if hunk.has_suspect(parent_id) {
318370
hunk.source_file_name = Some(source_location.clone());
371+
372+
has_blame_been_passed = true;
373+
}
374+
}
375+
376+
if has_blame_been_passed {
377+
if let Some(ref mut blame_path) = blame_path {
378+
let blame_path_entry = BlamePathEntry {
379+
source_file_path: current_file_path.clone(),
380+
previous_source_file_path: Some(source_location.clone()),
381+
commit_id: suspect,
382+
blob_id: id,
383+
previous_blob_id: source_id,
384+
parent_index: index,
385+
};
386+
blame_path.push(blame_path_entry);
319387
}
320388
}
321389
}
@@ -351,6 +419,7 @@ pub fn file(
351419
entries: coalesce_blame_entries(out),
352420
blob: blamed_file_blob,
353421
statistics: stats,
422+
blame_path,
354423
})
355424
}
356425

@@ -435,7 +504,9 @@ fn coalesce_blame_entries(lines_blamed: Vec<BlameEntry>) -> Vec<BlameEntry> {
435504
/// The union of [`gix_diff::tree::recorder::Change`] and [`gix_diff::tree_with_rewrites::Change`],
436505
/// keeping only the blame-relevant information.
437506
enum TreeDiffChange {
438-
Addition,
507+
Addition {
508+
id: ObjectId,
509+
},
439510
Deletion,
440511
Modification {
441512
previous_id: ObjectId,
@@ -453,7 +524,7 @@ impl From<gix_diff::tree::recorder::Change> for TreeDiffChange {
453524
use gix_diff::tree::recorder::Change;
454525

455526
match value {
456-
Change::Addition { .. } => Self::Addition,
527+
Change::Addition { oid, .. } => Self::Addition { id: oid },
457528
Change::Deletion { .. } => Self::Deletion,
458529
Change::Modification { previous_oid, oid, .. } => Self::Modification {
459530
previous_id: previous_oid,
@@ -468,7 +539,7 @@ impl From<gix_diff::tree_with_rewrites::Change> for TreeDiffChange {
468539
use gix_diff::tree_with_rewrites::Change;
469540

470541
match value {
471-
Change::Addition { .. } => Self::Addition,
542+
Change::Addition { id, .. } => Self::Addition { id },
472543
Change::Deletion { .. } => Self::Deletion,
473544
Change::Modification { previous_id, id, .. } => Self::Modification { previous_id, id },
474545
Change::Rewrite {

gix-blame/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
mod error;
1818
pub use error::Error;
1919
mod types;
20-
pub use types::{BlameEntry, BlameRanges, Options, Outcome, Statistics};
20+
pub use types::{BlameEntry, BlamePathEntry, BlameRanges, Options, Outcome, Statistics};
2121

2222
mod file;
2323
pub use file::function::file;

gix-blame/src/types.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,33 @@ pub struct Options {
149149
pub since: Option<gix_date::Time>,
150150
/// Determine if rename tracking should be performed, and how.
151151
pub rewrites: Option<gix_diff::Rewrites>,
152+
/// Collect debug information whenever there's a diff or rename that affects the outcome of a
153+
/// blame.
154+
pub debug_track_path: bool,
155+
}
156+
157+
/// Represents a change during history traversal for blame. It is supposed to capture enough
158+
/// information to allow reconstruction of the way a blame was performed, i. e. the path the
159+
/// history traversal, combined with repeated diffing of two subsequent states in this history, has
160+
/// taken.
161+
///
162+
/// This is intended for debugging purposes.
163+
#[derive(Clone, Debug)]
164+
pub struct BlamePathEntry {
165+
/// The path to the *Source File* in the blob after the change.
166+
pub source_file_path: BString,
167+
/// The path to the *Source File* in the blob before the change. Allows
168+
/// detection of renames. `None` for root commits.
169+
pub previous_source_file_path: Option<BString>,
170+
/// The commit id associated with the state after the change.
171+
pub commit_id: ObjectId,
172+
/// The blob id associated with the state after the change.
173+
pub blob_id: ObjectId,
174+
/// The blob id associated with the state before the change.
175+
pub previous_blob_id: ObjectId,
176+
/// When there is more than one `BlamePathEntry` for a commit, this indicates to which parent
177+
/// commit the change is related.
178+
pub parent_index: usize,
152179
}
153180

154181
/// The outcome of [`file()`](crate::file()).
@@ -161,6 +188,8 @@ pub struct Outcome {
161188
pub blob: Vec<u8>,
162189
/// Additional information about the amount of work performed to produce the blame.
163190
pub statistics: Statistics,
191+
/// Contains a log of all changes that affected the outcome of this blame.
192+
pub blame_path: Option<Vec<BlamePathEntry>>,
164193
}
165194

166195
/// Additional information about the performed operations.

gix-blame/tests/blame.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ macro_rules! mktest {
232232
range: BlameRanges::default(),
233233
since: None,
234234
rewrites: Some(gix_diff::Rewrites::default()),
235+
debug_track_path: false,
235236
},
236237
)?
237238
.entries;
@@ -317,6 +318,7 @@ fn diff_disparity() {
317318
range: BlameRanges::default(),
318319
since: None,
319320
rewrites: Some(gix_diff::Rewrites::default()),
321+
debug_track_path: false,
320322
},
321323
)
322324
.unwrap()
@@ -352,6 +354,7 @@ fn since() -> gix_testtools::Result {
352354
range: BlameRanges::default(),
353355
since: Some(gix_date::parse("2025-01-31", None)?),
354356
rewrites: Some(gix_diff::Rewrites::default()),
357+
debug_track_path: false,
355358
},
356359
)?
357360
.entries;
@@ -391,6 +394,7 @@ mod blame_ranges {
391394
range: BlameRanges::from_range(1..=2),
392395
since: None,
393396
rewrites: Some(gix_diff::Rewrites::default()),
397+
debug_track_path: false,
394398
},
395399
)?
396400
.entries;
@@ -431,6 +435,7 @@ mod blame_ranges {
431435
range: ranges,
432436
since: None,
433437
rewrites: None,
438+
debug_track_path: false,
434439
},
435440
)?
436441
.entries;
@@ -471,6 +476,7 @@ mod blame_ranges {
471476
range: ranges,
472477
since: None,
473478
rewrites: None,
479+
debug_track_path: false,
474480
},
475481
)?
476482
.entries;
@@ -516,6 +522,7 @@ mod rename_tracking {
516522
range: BlameRanges::default(),
517523
since: None,
518524
rewrites: Some(gix_diff::Rewrites::default()),
525+
debug_track_path: false,
519526
},
520527
)?
521528
.entries;

gix-config/tests/config/parse/section.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ mod header {
1111

1212
use bstr::BStr;
1313

14-
fn cow_section(name: &str) -> Option<Cow<BStr>> {
14+
fn cow_section(name: &str) -> Option<Cow<'_, BStr>> {
1515
Some(Cow::Borrowed(name.into()))
1616
}
1717
mod write_to {

gix-odb/src/store_impls/dynamic/load_index.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,7 @@ impl PartialEq<Self> for Either {
734734
}
735735
}
736736

737+
#[allow(clippy::non_canonical_partial_ord_impl)]
737738
impl PartialOrd<Self> for Either {
738739
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
739740
Some(self.path().cmp(other.path()))

gix-ref/src/name.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ impl<'a> convert::TryFrom<&'a str> for PartialName {
221221
}
222222
}
223223

224+
#[allow(clippy::infallible_try_from)]
224225
impl<'a> convert::TryFrom<&'a FullName> for &'a PartialNameRef {
225226
type Error = Infallible;
226227

gix-refspec/src/spec.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ mod impls {
6969
}
7070
}
7171

72+
#[allow(clippy::non_canonical_partial_ord_impl)]
7273
impl PartialOrd for RefSpec {
7374
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
7475
Some(self.to_ref().cmp(&other.to_ref()))

0 commit comments

Comments
 (0)