diff --git a/gitoxide-core/src/repository/blame.rs b/gitoxide-core/src/repository/blame.rs index 2d2930b30dd..a783f9e4d39 100644 --- a/gitoxide-core/src/repository/blame.rs +++ b/gitoxide-core/src/repository/blame.rs @@ -43,6 +43,7 @@ pub fn blame_file( suspect, cache, &mut resource_cache, + None, file.as_bstr(), range, )?; diff --git a/gix-blame/src/file/function.rs b/gix-blame/src/file/function.rs index 576611896d6..95d0b19dad4 100644 --- a/gix-blame/src/file/function.rs +++ b/gix-blame/src/file/function.rs @@ -1,10 +1,11 @@ -use super::{process_changes, Change, UnblamedHunk}; +use super::{process_changes, update_blame_with_changes, Change, UnblamedHunk}; +use crate::types::BlameCacheObject; use crate::{BlameEntry, Error, Outcome, Statistics}; use gix_diff::blob::intern::TokenSource; use gix_diff::tree::Visit; use gix_hash::ObjectId; use gix_object::{ - bstr::{BStr, BString}, + bstr::{BStr, BString, ByteSlice}, FindExt, }; use gix_traverse::commit::find as find_commit; @@ -66,6 +67,7 @@ pub fn file( suspect: ObjectId, cache: Option, resource_cache: &mut gix_diff::blob::Platform, + blame_cache: Option, file_path: &BStr, range: Option>, ) -> Result { @@ -73,19 +75,15 @@ pub fn file( let mut stats = Statistics::default(); let (mut buf, mut buf2, mut buf3) = (Vec::new(), Vec::new(), Vec::new()); - let blamed_file_entry_id = find_path_entry_in_commit( - &odb, - &suspect, - file_path, - cache.as_ref(), - &mut buf, - &mut buf2, - &mut stats, - )? - .ok_or_else(|| Error::FileMissing { - file_path: file_path.to_owned(), - commit_id: suspect, - })?; + let mut file_id = |commit, buf: &mut Vec, buf2: &mut Vec| { + find_path_entry_in_commit(&odb, commit, file_path, cache.as_ref(), buf, buf2, &mut stats)?.ok_or_else(|| { + Error::FileMissing { + file_path: file_path.to_owned(), + commit_id: suspect, + } + }) + }; + let blamed_file_entry_id = file_id(&suspect, &mut buf, &mut buf2)?; let blamed_file_blob = odb.find_blob(&blamed_file_entry_id, &mut buf)?.data.to_vec(); let num_lines_in_blamed = tokens_for_diffing(&blamed_file_blob).tokenize().count() as u32; @@ -95,17 +93,56 @@ pub fn file( } let range_in_blamed_file = one_based_inclusive_to_zero_based_exclusive_range(range, num_lines_in_blamed)?; - let mut hunks_to_blame = vec![UnblamedHunk { - range_in_blamed_file: range_in_blamed_file.clone(), - suspects: [(suspect, range_in_blamed_file)].into(), - }]; + + let (blame_entries, mut hunks_to_blame) = match blame_cache { + Some(blame_cache) => { + // If there is a cache, we first get the diff between the current commit and the commit + // we passed as the cache. + let old_file_id = file_id(&blame_cache.cache_id, &mut buf, &mut buf2)?; + let changes = blob_changes( + &odb, + resource_cache, + blamed_file_entry_id, + old_file_id, + file_path.as_bstr(), + &mut stats, + )?; + + // If there are no changes, we can return the cache as is immediately. + if changes.iter().all(|change| matches!(change, Change::Unchanged(_))) { + return Ok(Outcome { + entries: blame_cache.entries.clone(), + blob: blamed_file_blob, + statistics: stats, + }); + } + // Otherwise, we update the cache with the new changes. + let (blame_entries, hunks_to_blame) = update_blame_with_changes(blame_cache.entries, changes, suspect); + // If there are no more hunks to blame, we can return the result immediately. + if hunks_to_blame.is_empty() { + return Ok(Outcome { + entries: blame_entries, + blob: blamed_file_blob, + statistics: stats, + }); + } + (blame_entries, hunks_to_blame) + } + None => { + let hunks_to_blame = vec![UnblamedHunk { + range_in_blamed_file: range_in_blamed_file.clone(), + suspects: [(suspect, range_in_blamed_file)].into(), + }]; + (Vec::new(), hunks_to_blame) + } + }; let (mut buf, mut buf2) = (Vec::new(), Vec::new()); let commit = find_commit(cache.as_ref(), &odb, &suspect, &mut buf)?; let mut queue: gix_revwalk::PriorityQueue = gix_revwalk::PriorityQueue::new(); queue.insert(commit_time(commit)?, suspect); - let mut out = Vec::new(); + let mut out = blame_entries; let mut diff_state = gix_diff::tree::State::default(); let mut previous_entry: Option<(ObjectId, ObjectId)> = None; 'outer: while let Some(suspect) = queue.pop_value() { diff --git a/gix-blame/src/file/mod.rs b/gix-blame/src/file/mod.rs index 668be563533..8459874966d 100644 --- a/gix-blame/src/file/mod.rs +++ b/gix-blame/src/file/mod.rs @@ -5,7 +5,7 @@ use std::ops::Range; use gix_hash::ObjectId; -use crate::types::{BlameEntry, Either, LineRange}; +use crate::types::{BlameEntry, BlameLines, ChangeLines, Either, LineRange}; use crate::types::{Change, Offset, UnblamedHunk}; pub(super) mod function; @@ -357,6 +357,147 @@ fn process_changes( new_hunks_to_blame } +/// Consume `cached_blames` and `changes`. With the changes we update the cached blames. +/// This function returns the updated blames and the new hunks to blame. +fn update_blame_with_changes( + cached_blames: Vec, + changes: Vec, + head_id: ObjectId, +) -> (Vec, Vec) { + fn blame_fully_contained_by_change( + blame_lines: &BlameLines, + blame: &BlameEntry, + change_lines: &ChangeLines, + change: &Change, + ) -> bool { + blame_lines.get_remaining(blame) < change_lines.get_remaining(change) + } + + let mut updated_blames = Vec::new(); + let mut new_hunks_to_blame = Vec::new(); + + let mut blame_iter = cached_blames.into_iter().peekable(); + + // This is a nested loop where we iterate over the changes and the blames. + // We keep track of the assigned lines in the change and the blame. + // For each of the three possible cases (Unchanged, Deleted, AddedOrReplaced) we have different + // rules for how to update the blame. + 'change: for change in changes { + let mut change_assigned = ChangeLines::default(); + while let Some(blame) = blame_iter.peek_mut() { + let mut blame_assigned = BlameLines::default(); + + // For each of the three cases we have to check if the blame is fully contained by the change. + // If so we can update the blame with the remaining length of the blame. + // If not we have to update the blame with the remaining length of the change. + match change { + Change::Unchanged(ref range) => { + match blame_fully_contained_by_change(&blame_assigned, blame, &change_assigned, &change) { + true => { + updated_blames.push(BlameEntry { + start_in_blamed_file: range.start + change_assigned.assigned.get_assigned(), + start_in_source_file: blame.start_in_source_file, + len: blame.len, + commit_id: blame.commit_id, + }); + + change_assigned.assigned.add_assigned(blame.len.get()); + blame_assigned.assigned.add_assigned(blame.len.get()); + } + false => { + updated_blames.push(BlameEntry { + start_in_blamed_file: range.start + change_assigned.assigned.get_assigned(), + start_in_source_file: blame.start_in_source_file, + len: NonZeroU32::new(change_assigned.get_remaining(&change)).unwrap(), + commit_id: blame.commit_id, + }); + + blame_assigned + .assigned + .add_assigned(change_assigned.get_remaining(&change)); + change_assigned + .assigned + .add_assigned(change_assigned.get_remaining(&change)); + } + } + } + Change::Deleted(_start_deletion, _lines_deleted) => { + match blame_fully_contained_by_change(&blame_assigned, blame, &change_assigned, &change) { + true => { + blame_assigned.assigned.add_assigned(blame.len.get()); + change_assigned.assigned.add_assigned(blame.len.get()); + } + false => { + blame_assigned + .assigned + .add_assigned(change_assigned.get_remaining(&change)); + change_assigned + .assigned + .add_assigned(change_assigned.get_remaining(&change)); + } + } + } + Change::AddedOrReplaced(ref range, lines_deleted) => { + let new_unblamed_hunk = |range: &Range, head_id: ObjectId| UnblamedHunk { + range_in_blamed_file: range.clone(), + suspects: [(head_id, range.clone())].into(), + }; + match blame_fully_contained_by_change(&blame_assigned, blame, &change_assigned, &change) { + true => { + if lines_deleted == 0 { + new_hunks_to_blame.push(new_unblamed_hunk(range, head_id)); + } + + change_assigned.assigned.add_assigned(blame.len.get()); + blame_assigned.assigned.add_assigned(blame.len.get()); + } + false => { + new_hunks_to_blame.push(new_unblamed_hunk(range, head_id)); + + blame_assigned + .assigned + .add_assigned(change_assigned.get_remaining(&change)); + change_assigned + .assigned + .add_assigned(change_assigned.get_remaining(&change)); + } + } + } + } + + // Check if the blame or the change is fully assigned. + // If the blame is fully assigned we can continue with the next blame. + // If the change is fully assigned we can continue with the next change. + // Since we have a mutable reference to the blame we can update it and reset the assigned blame lines. + // If both are fully assigned we can continue with the next blame and change. + match ( + blame_assigned.has_remaining(blame), + change_assigned.has_remaining(&change), + ) { + (true, true) => { + // Both have remaining + blame.update_blame(&blame_assigned.assigned); + } + (true, false) => { + // Change is fully assigned + blame.update_blame(&blame_assigned.assigned); + continue 'change; + } + (false, true) => { + // Blame is fully assigned + blame_iter.next(); + } + (false, false) => { + // Both are fully assigned + blame_iter.next(); + continue 'change; + } + }; + } + } + (updated_blames, new_hunks_to_blame) +} + impl UnblamedHunk { fn shift_by(mut self, suspect: ObjectId, offset: Offset) -> Self { self.suspects.entry(suspect).and_modify(|e| *e = e.shift_by(offset)); diff --git a/gix-blame/src/file/tests.rs b/gix-blame/src/file/tests.rs index 1366fcfea9c..f5f75e8d980 100644 --- a/gix-blame/src/file/tests.rs +++ b/gix-blame/src/file/tests.rs @@ -27,6 +27,246 @@ fn one_sha() -> ObjectId { ObjectId::from_str("1111111111111111111111111111111111111111").unwrap() } +fn two_sha() -> ObjectId { + use std::str::FromStr; + + ObjectId::from_str("2222222222222222222222222222222222222222").unwrap() +} + +mod blame_cache_to_hunks { + use super::*; + use crate::file::{update_blame_with_changes, BlameEntry, Change, UnblamedHunk}; + + fn single_blame_entry() -> Vec { + vec![BlameEntry::new(0..5, 0..5, zero_sha())] + } + + fn multiple_blame_entries() -> Vec { + vec![ + BlameEntry::new(0..5, 0..5, zero_sha()), + BlameEntry::new(5..10, 0..5, one_sha()), + ] + } + + #[test] + fn no_changes() { + let cached_blames = single_blame_entry(); + let changes = vec![Change::Unchanged(0..5)]; + + let expected_blame = single_blame_entry(); + let (updated_blame_entries, new_unblamed_hunks) = update_blame_with_changes(cached_blames, changes, one_sha()); + assert_eq!(updated_blame_entries, expected_blame); + assert!(new_unblamed_hunks.is_empty()); + } + + #[test] + fn deleted_line_full_blame() { + let cached_blames = single_blame_entry(); + let changes = vec![Change::Deleted(0, 5)]; + let expected_blame = vec![]; + let (updated_blame_entries, new_unblamed_hunks) = update_blame_with_changes(cached_blames, changes, one_sha()); + assert_eq!(updated_blame_entries, expected_blame); + assert!(new_unblamed_hunks.is_empty()); + } + + #[test] + fn deleted_line_partial_first_delete() { + let cached_blames = single_blame_entry(); + let changes = vec![Change::Deleted(0, 3), Change::Unchanged(0..2)]; + let expected_blame = vec![BlameEntry::new(0..2, 3..5, zero_sha())]; + let (updated_blame_entries, new_unblamed_hunks) = update_blame_with_changes(cached_blames, changes, one_sha()); + assert_eq!(updated_blame_entries, expected_blame); + assert!(new_unblamed_hunks.is_empty()); + } + + #[test] + fn deleted_line_partial_first_unchanged() { + let cached_blames = single_blame_entry(); + let changes = vec![Change::Unchanged(0..2), Change::Deleted(0, 3)]; + let expected_blame = vec![BlameEntry::new(0..2, 0..2, zero_sha())]; + let (updated_blame_entries, new_unblamed_hunks) = update_blame_with_changes(cached_blames, changes, one_sha()); + assert_eq!(updated_blame_entries, expected_blame); + assert!(new_unblamed_hunks.is_empty()); + } + + #[test] + fn deleted_line_spanning() { + let cached_blames = single_blame_entry(); + let changes = vec![Change::Unchanged(0..1), Change::Deleted(1, 3), Change::Unchanged(1..2)]; + let expected_blame = vec![ + BlameEntry::new(0..1, 0..1, zero_sha()), + BlameEntry::new(1..2, 4..5, zero_sha()), + ]; + let (updated_blame_entries, new_unblamed_hunks) = update_blame_with_changes(cached_blames, changes, one_sha()); + assert_eq!(updated_blame_entries, expected_blame); + assert!(new_unblamed_hunks.is_empty()); + } + + #[test] + fn add_or_replace_full_blame_delete() { + let cached_blames = single_blame_entry(); + let changes = vec![Change::AddedOrReplaced(0..5, 5)]; + let expected_blame = vec![]; + let expected_unblamed_hunks = vec![UnblamedHunk { + range_in_blamed_file: 0..5, + suspects: [(two_sha(), 0..5)].into(), + }]; + let (updated_blame_entries, new_unblamed_hunks) = update_blame_with_changes(cached_blames, changes, two_sha()); + assert_eq!(updated_blame_entries, expected_blame); + assert_eq!(new_unblamed_hunks, expected_unblamed_hunks); + } + + #[test] + fn add_or_replace_first_partial_blame_delete() { + let cached_blames = single_blame_entry(); + let changes = vec![Change::AddedOrReplaced(0..5, 3), Change::Unchanged(5..7)]; + let expected_blame = vec![BlameEntry::new(5..7, 3..5, zero_sha())]; + let expected_unblamed_hunks = vec![UnblamedHunk { + range_in_blamed_file: 0..5, + suspects: [(two_sha(), 0..5)].into(), + }]; + let (updated_blame_entries, new_unblamed_hunks) = update_blame_with_changes(cached_blames, changes, two_sha()); + assert_eq!(updated_blame_entries, expected_blame); + assert_eq!(new_unblamed_hunks, expected_unblamed_hunks); + } + + #[test] + fn add_or_replace_first_unchanged_partial_blame_delete() { + let cached_blames = single_blame_entry(); + let changes = vec![Change::Unchanged(0..3), Change::AddedOrReplaced(0..5, 2)]; + let expected_blame = vec![BlameEntry::new(0..3, 0..3, zero_sha())]; + let expected_unblamed_hunks = vec![UnblamedHunk { + range_in_blamed_file: 0..5, + suspects: [(two_sha(), 0..5)].into(), + }]; + let (updated_blame_entries, new_unblamed_hunks) = update_blame_with_changes(cached_blames, changes, two_sha()); + assert_eq!(updated_blame_entries, expected_blame); + assert_eq!(new_unblamed_hunks, expected_unblamed_hunks); + } + + #[test] + fn add_or_replace_unchanged_replace_unchanged() { + let cached_blames = single_blame_entry(); + let changes = vec![ + Change::Unchanged(0..2), + Change::AddedOrReplaced(0..5, 1), + Change::Unchanged(7..9), + ]; + let expected_blame = vec![ + BlameEntry::new(0..2, 0..2, zero_sha()), + BlameEntry::new(7..9, 3..5, zero_sha()), + ]; + let expected_unblamed_hunks = vec![UnblamedHunk { + range_in_blamed_file: 0..5, + suspects: [(two_sha(), 0..5)].into(), + }]; + let (updated_blame_entries, new_unblamed_hunks) = update_blame_with_changes(cached_blames, changes, two_sha()); + assert_eq!(updated_blame_entries, expected_blame); + assert_eq!(new_unblamed_hunks, expected_unblamed_hunks); + } + + #[test] + fn add_or_replace_no_deletion() { + let cached_blames = single_blame_entry(); + let changes = vec![ + Change::Unchanged(0..2), + Change::AddedOrReplaced(0..5, 0), + Change::Unchanged(6..9), + ]; + let expected_blame = vec![ + BlameEntry::new(0..2, 0..2, zero_sha()), + BlameEntry::new(6..9, 2..5, zero_sha()), + ]; + let expected_unblamed_hunks = vec![UnblamedHunk { + range_in_blamed_file: 0..5, + suspects: [(two_sha(), 0..5)].into(), + }]; + let (updated_blame_entries, new_unblamed_hunks) = update_blame_with_changes(cached_blames, changes, two_sha()); + assert_eq!(updated_blame_entries, expected_blame); + assert_eq!(new_unblamed_hunks, expected_unblamed_hunks); + } + + #[test] + fn multiple_blames_no_change() { + let cached_blames = multiple_blame_entries(); + let changes = vec![Change::Unchanged(0..10)]; + let expected_blame = vec![ + BlameEntry::new(0..5, 0..5, zero_sha()), + BlameEntry::new(5..10, 0..5, one_sha()), + ]; + let (updated_blame_entries, new_unblamed_hunks) = update_blame_with_changes(cached_blames, changes, one_sha()); + assert_eq!(updated_blame_entries, expected_blame); + assert!(new_unblamed_hunks.is_empty()); + } + + #[test] + fn multiple_blames_change_spans_multiple_lines_first_unchanged() { + let cached_blames = multiple_blame_entries(); + let changes = vec![Change::Unchanged(0..6), Change::Deleted(6, 4)]; + let expected_blame = vec![ + BlameEntry::new(0..5, 0..5, zero_sha()), + BlameEntry::new(5..6, 0..1, one_sha()), + ]; + let (updated_blame_entries, new_unblamed_hunks) = update_blame_with_changes(cached_blames, changes, one_sha()); + assert_eq!(updated_blame_entries, expected_blame); + assert!(new_unblamed_hunks.is_empty()); + } + + #[test] + fn multiple_blames_change_spans_multiple_lines_first_delete() { + let cached_blames = multiple_blame_entries(); + let changes = vec![Change::Deleted(0, 4), Change::Unchanged(0..6)]; + let expected_blame = vec![ + BlameEntry::new(0..1, 4..5, zero_sha()), + BlameEntry::new(1..6, 0..5, one_sha()), + ]; + let (updated_blame_entries, new_unblamed_hunks) = update_blame_with_changes(cached_blames, changes, one_sha()); + assert_eq!(updated_blame_entries, expected_blame); + assert!(new_unblamed_hunks.is_empty()); + } + + #[test] + fn multiple_blames_change_spans_delete_spanning() { + let cached_blames = multiple_blame_entries(); + let changes = vec![Change::Unchanged(0..4), Change::Deleted(4, 4), Change::Unchanged(4..6)]; + let expected_blame = vec![ + BlameEntry::new(0..4, 0..4, zero_sha()), + BlameEntry::new(4..6, 3..5, one_sha()), + ]; + let (updated_blame_entries, new_unblamed_hunks) = update_blame_with_changes(cached_blames, changes, one_sha()); + assert_eq!(updated_blame_entries, expected_blame); + assert!(new_unblamed_hunks.is_empty()); + } + + #[test] + fn multiple_blames_add_or_replace_blame_delete_spanning() { + let cached_blames = multiple_blame_entries(); + let changes = vec![ + Change::Unchanged(0..4), + Change::AddedOrReplaced(4..9, 3), + Change::Unchanged(9..10), + Change::AddedOrReplaced(10..12, 2), + ]; + let expected_blame = vec![ + BlameEntry::new(0..4, 0..4, zero_sha()), + BlameEntry::new(9..10, 2..3, one_sha()), + ]; + let expected_unblamed_hunks = vec![ + UnblamedHunk { + range_in_blamed_file: 4..9, + suspects: [(two_sha(), 4..9)].into(), + }, + UnblamedHunk { + range_in_blamed_file: 10..12, + suspects: [(two_sha(), 10..12)].into(), + }, + ]; + let (updated_blame_entries, new_unblamed_hunks) = update_blame_with_changes(cached_blames, changes, two_sha()); + assert_eq!(updated_blame_entries, expected_blame); + assert_eq!(new_unblamed_hunks, expected_unblamed_hunks); + } +} + mod process_change { use super::*; use crate::file::{process_change, Change, Offset, UnblamedHunk}; diff --git a/gix-blame/src/lib.rs b/gix-blame/src/lib.rs index 489434b5b3d..e0bb3671cba 100644 --- a/gix-blame/src/lib.rs +++ b/gix-blame/src/lib.rs @@ -17,7 +17,7 @@ mod error; pub use error::Error; mod types; -pub use types::{BlameEntry, Outcome, Statistics}; +pub use types::{BlameCacheObject, BlameEntry, Outcome, Statistics}; mod file; pub use file::function::file; diff --git a/gix-blame/src/types.rs b/gix-blame/src/types.rs index f539b11ed87..ae9f3de9c2e 100644 --- a/gix-blame/src/types.rs +++ b/gix-blame/src/types.rs @@ -117,6 +117,15 @@ impl SubAssign for Offset { } } +#[derive(Debug, PartialEq)] +/// A cache of blame entries that can be used to speed up subsequent blames. +pub struct BlameCacheObject { + /// The entries of the cache. + pub entries: Vec, + /// The commit that was blamed to produce these entries. + pub cache_id: ObjectId, +} + /// A mapping of a section of the *Blamed File* to the section in a *Source File* that introduced it. /// /// Both ranges are of the same size, but may use different [starting points](Range::start). Naturally, @@ -170,6 +179,15 @@ impl BlameEntry { } } +impl BlameEntry { + /// Update the `start_in_blamed_file` and `start_in_source_file` fields by the lines that are already assigned. + pub(crate) fn update_blame(&mut self, offset: &LinesAssigned) { + self.start_in_blamed_file += offset.get_assigned(); + self.start_in_source_file += offset.get_assigned(); + self.len = NonZeroU32::new(u32::from(self.len) - offset.get_assigned()).unwrap(); + } +} + pub(crate) trait LineRange { fn shift_by(&self, offset: Offset) -> Self; } @@ -205,3 +223,52 @@ pub enum Change { /// `(line_to_start_deletion_at, num_deleted_in_before)` Deleted(u32, u32), } + +#[derive(Debug, Default)] +pub(crate) struct LinesAssigned { + lines_assigned: u32, +} + +impl LinesAssigned { + pub(crate) fn add_assigned(&mut self, lines: u32) { + self.lines_assigned += lines; + } + + pub(crate) fn get_assigned(&self) -> u32 { + self.lines_assigned + } +} + +#[derive(Debug, Default)] +pub(crate) struct BlameLines { + pub(crate) assigned: LinesAssigned, +} + +impl BlameLines { + pub(crate) fn get_remaining(&self, blame: &BlameEntry) -> u32 { + blame.len.get() - self.assigned.get_assigned() + } + + pub(crate) fn has_remaining(&self, blame: &BlameEntry) -> bool { + self.get_remaining(blame) > 0 + } +} + +#[derive(Debug, Default)] +pub(crate) struct ChangeLines { + pub(crate) assigned: LinesAssigned, +} + +impl ChangeLines { + pub(crate) fn get_remaining(&self, change: &Change) -> u32 { + match &change { + Change::Unchanged(range) => range.len() as u32 - self.assigned.get_assigned(), + Change::AddedOrReplaced(_, deleted_in_before) => *deleted_in_before - self.assigned.get_assigned(), + Change::Deleted(_, deleted_in_before) => *deleted_in_before - self.assigned.get_assigned(), + } + } + + pub(crate) fn has_remaining(&self, change: &Change) -> bool { + self.get_remaining(change) > 0 + } +} diff --git a/gix-blame/tests/blame.rs b/gix-blame/tests/blame.rs index 926118ef432..7329f5a6e71 100644 --- a/gix-blame/tests/blame.rs +++ b/gix-blame/tests/blame.rs @@ -190,6 +190,7 @@ macro_rules! mktest { suspect, None, &mut resource_cache, + None, format!("{}.txt", $case).as_str().into(), None, )? @@ -257,6 +258,7 @@ fn diff_disparity() { suspect, None, &mut resource_cache, + None, format!("{case}.txt").as_str().into(), None, ) @@ -285,6 +287,7 @@ fn line_range() { suspect, None, &mut resource_cache, + None, "simple.txt".into(), Some(1..2), )