use std::cmp;
use std::collections::HashMap;
use std::io::Read;
use std::ops::Range;
use std::rc::Rc;
use bstr::BString;
use futures::StreamExt as _;
use itertools::Itertools as _;
use thiserror::Error;
use crate::annotate::get_annotation_with_file_content;
use crate::backend::BackendError;
use crate::backend::BackendResult;
use crate::backend::CommitId;
use crate::backend::FileId;
use crate::backend::TreeValue;
use crate::commit::Commit;
use crate::conflicts::materialized_diff_stream;
use crate::conflicts::MaterializedTreeValue;
use crate::copies::CopyRecords;
use crate::diff::Diff;
use crate::diff::DiffHunkKind;
use crate::matchers::Matcher;
use crate::merge::Merge;
use crate::merged_tree::MergedTree;
use crate::merged_tree::MergedTreeBuilder;
use crate::repo::MutableRepo;
use crate::repo::Repo;
use crate::repo_path::RepoPath;
use crate::repo_path::RepoPathBuf;
use crate::revset::ResolvedRevsetExpression;
use crate::revset::RevsetEvaluationError;
#[derive(Clone, Debug)]
pub struct AbsorbSource {
commit: Commit,
parent_tree: MergedTree,
}
impl AbsorbSource {
pub fn from_commit(repo: &dyn Repo, commit: Commit) -> BackendResult<Self> {
let parent_tree = commit.parent_tree(repo)?;
Ok(AbsorbSource {
commit,
parent_tree,
})
}
}
#[derive(Debug, Error)]
pub enum AbsorbError {
#[error(transparent)]
Backend(#[from] BackendError),
#[error(transparent)]
RevsetEvaluation(#[from] RevsetEvaluationError),
}
#[derive(Default)]
pub struct SelectedTrees {
pub target_commits: HashMap<CommitId, MergedTreeBuilder>,
pub skipped_paths: Vec<(RepoPathBuf, String)>,
}
pub async fn split_hunks_to_trees(
repo: &dyn Repo,
source: &AbsorbSource,
destinations: &Rc<ResolvedRevsetExpression>,
matcher: &dyn Matcher,
) -> Result<SelectedTrees, AbsorbError> {
let mut selected_trees = SelectedTrees::default();
let left_tree = &source.parent_tree;
let right_tree = source.commit.tree()?;
let copy_records = CopyRecords::default();
let tree_diff = left_tree.diff_stream_with_copies(&right_tree, matcher, ©_records);
let mut diff_stream = materialized_diff_stream(repo.store(), tree_diff);
while let Some(entry) = diff_stream.next().await {
let left_path = entry.path.source();
let right_path = entry.path.target();
let (left_value, right_value) = entry.values?;
let (left_text, executable) = match to_file_value(left_value) {
Ok(Some(mut value)) => (value.read(left_path)?, value.executable),
Ok(None) => continue,
Err(reason) => {
selected_trees
.skipped_paths
.push((left_path.to_owned(), reason));
continue;
}
};
let right_text = match to_file_value(right_value) {
Ok(Some(mut value)) => value.read(right_path)?,
Ok(None) => continue,
Err(reason) => {
selected_trees
.skipped_paths
.push((right_path.to_owned(), reason));
continue;
}
};
let annotation = get_annotation_with_file_content(
repo,
source.commit.id(),
destinations,
left_path,
left_text.clone(),
)?;
let annotation_ranges = annotation
.compact_line_ranges()
.filter_map(|(commit_id, range)| Some((commit_id?, range)))
.collect_vec();
let diff = Diff::by_line([&left_text, &right_text]);
let selected_ranges = split_file_hunks(&annotation_ranges, &diff);
for (&commit_id, ranges) in &selected_ranges {
let tree_builder = selected_trees
.target_commits
.entry(commit_id.clone())
.or_insert_with(|| MergedTreeBuilder::new(left_tree.id().clone()));
let new_text = combine_texts(&left_text, &right_text, ranges);
let id = repo
.store()
.write_file(left_path, &mut new_text.as_slice())
.await?;
tree_builder.set_or_remove(
left_path.to_owned(),
Merge::normal(TreeValue::File { id, executable }),
);
}
}
Ok(selected_trees)
}
type SelectedRange = (Range<usize>, Range<usize>);
fn split_file_hunks<'a>(
mut annotation_ranges: &[(&'a CommitId, Range<usize>)],
diff: &Diff,
) -> HashMap<&'a CommitId, Vec<SelectedRange>> {
debug_assert!(annotation_ranges.iter().all(|(_, range)| !range.is_empty()));
let mut selected_ranges: HashMap<&CommitId, Vec<_>> = HashMap::new();
let mut diff_hunk_ranges = diff
.hunk_ranges()
.filter(|hunk| hunk.kind == DiffHunkKind::Different);
while !annotation_ranges.is_empty() {
let Some(hunk) = diff_hunk_ranges.next() else {
break;
};
let [left_range, right_range]: &[_; 2] = hunk.ranges[..].try_into().unwrap();
assert!(!left_range.is_empty() || !right_range.is_empty());
if right_range.is_empty() {
let skip = annotation_ranges
.iter()
.take_while(|(_, range)| range.end <= left_range.start)
.count();
annotation_ranges = &annotation_ranges[skip..];
let pre_overlap = annotation_ranges
.iter()
.take_while(|(_, range)| range.end < left_range.end)
.count();
let maybe_overlapped_ranges = annotation_ranges.get(..pre_overlap + 1);
annotation_ranges = &annotation_ranges[pre_overlap..];
let Some(overlapped_ranges) = maybe_overlapped_ranges else {
continue;
};
let all_covered = overlapped_ranges
.iter()
.try_fold(left_range.start, |prev_end, (_, cur)| {
(cur.start <= prev_end).then_some(cur.end)
})
.inspect(|&last_end| assert!(left_range.end <= last_end))
.is_some();
if all_covered {
for (commit_id, cur_range) in overlapped_ranges {
let start = cmp::max(cur_range.start, left_range.start);
let end = cmp::min(cur_range.end, left_range.end);
assert!(start < end);
let selected = selected_ranges.entry(commit_id).or_default();
selected.push((start..end, right_range.clone()));
}
}
} else {
let skip = annotation_ranges
.iter()
.take_while(|(_, range)| range.end < left_range.end)
.count();
annotation_ranges = &annotation_ranges[skip..];
let Some((commit_id, cur_range)) = annotation_ranges.first() else {
continue;
};
let contained = cur_range.start <= left_range.start && left_range.end <= cur_range.end;
let ambiguous = cur_range.end == left_range.start
&& annotation_ranges
.get(1)
.is_some_and(|(_, next_range)| next_range.start == left_range.end);
if contained && !ambiguous {
let selected = selected_ranges.entry(commit_id).or_default();
selected.push((left_range.clone(), right_range.clone()));
}
}
}
selected_ranges
}
fn combine_texts(text1: &[u8], text2: &[u8], selected_ranges: &[SelectedRange]) -> BString {
itertools::chain!(
[(0..0, 0..0)],
selected_ranges.iter().cloned(),
[(text1.len()..text1.len(), text2.len()..text2.len())],
)
.tuple_windows()
.map(|((prev1, _), (cur1, cur2))| (prev1.end..cur1.start, cur2))
.flat_map(|(range1, range2)| [&text1[range1], &text2[range2]])
.collect()
}
pub fn absorb_hunks(
repo: &mut MutableRepo,
source: &AbsorbSource,
mut selected_trees: HashMap<CommitId, MergedTreeBuilder>,
) -> BackendResult<(Vec<Commit>, usize)> {
let store = repo.store().clone();
let mut rewritten_commits = Vec::new();
let mut num_rebased = 0;
repo.transform_descendants(selected_trees.keys().cloned().collect(), |rewriter| {
if rewriter.old_commit().id() == source.commit.id() {
let commit_builder = rewriter.reparent();
if commit_builder.is_discardable()? {
commit_builder.abandon();
} else {
commit_builder.write()?;
num_rebased += 1;
}
return Ok(());
}
let Some(tree_builder) = selected_trees.remove(rewriter.old_commit().id()) else {
rewriter.rebase()?.write()?;
num_rebased += 1;
return Ok(());
};
let selected_tree_id = tree_builder.write_tree(&store)?;
let commit_builder = rewriter.rebase()?;
let destination_tree = store.get_root_tree(commit_builder.tree_id())?;
let selected_tree = store.get_root_tree(&selected_tree_id)?;
let new_tree = destination_tree.merge(&source.parent_tree, &selected_tree)?;
let mut predecessors = commit_builder.predecessors().to_vec();
predecessors.push(source.commit.id().clone());
let new_commit = commit_builder
.set_tree_id(new_tree.id())
.set_predecessors(predecessors)
.write()?;
rewritten_commits.push(new_commit);
Ok(())
})?;
Ok((rewritten_commits, num_rebased))
}
struct FileValue {
id: FileId,
executable: bool,
reader: Box<dyn Read>,
}
impl FileValue {
fn read(&mut self, path: &RepoPath) -> BackendResult<BString> {
let mut buf = Vec::new();
self.reader
.read_to_end(&mut buf)
.map_err(|err| BackendError::ReadFile {
path: path.to_owned(),
id: self.id.clone(),
source: err.into(),
})?;
Ok(buf.into())
}
}
fn to_file_value(value: MaterializedTreeValue) -> Result<Option<FileValue>, String> {
match value {
MaterializedTreeValue::Absent => Ok(None), MaterializedTreeValue::AccessDenied(err) => Err(format!("Access is denied: {err}")),
MaterializedTreeValue::File {
id,
executable,
reader,
} => Ok(Some(FileValue {
id,
executable,
reader,
})),
MaterializedTreeValue::Symlink { .. } => Err("Is a symlink".into()),
MaterializedTreeValue::FileConflict { .. }
| MaterializedTreeValue::OtherConflict { .. } => Err("Is a conflict".into()),
MaterializedTreeValue::GitSubmodule(_) => Err("Is a Git submodule".into()),
MaterializedTreeValue::Tree(_) => panic!("diff should not contain trees"),
}
}
#[cfg(test)]
mod tests {
use maplit::hashmap;
use super::*;
#[test]
fn test_split_file_hunks_empty_or_single_line() {
let commit_id1 = &CommitId::from_hex("111111");
assert_eq!(split_file_hunks(&[], &Diff::by_line(["", ""])), hashmap! {});
assert_eq!(
split_file_hunks(&[], &Diff::by_line(["", "2X\n"])),
hashmap! {}
);
assert_eq!(
split_file_hunks(&[(commit_id1, 0..3)], &Diff::by_line(["1a\n", ""])),
hashmap! { commit_id1 => vec![(0..3, 0..0)] }
);
assert_eq!(
split_file_hunks(&[(commit_id1, 0..3)], &Diff::by_line(["1a\n", "1AA\n"])),
hashmap! { commit_id1 => vec![(0..3, 0..4)] }
);
}
#[test]
fn test_split_file_hunks_single_range() {
let commit_id1 = &CommitId::from_hex("111111");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6)],
&Diff::by_line(["1a\n1b\n", "1X\n1a\n1Y\n1b\n1Z\n"])
),
hashmap! {
commit_id1 => vec![(0..0, 0..3), (3..3, 6..9), (6..6, 12..15)],
}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..15)],
&Diff::by_line(["1a\n1b\n1c\n1d\n1e\n1f\n", "1b\n1d\n1f\n"])
),
hashmap! {
commit_id1 => vec![(0..3, 0..0), (6..9, 3..3), (12..15, 6..6)],
}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..12)],
&Diff::by_line(["1a\n1b\n1c\n1d\n", "1A\n1b\n1C\n1d\n"])
),
hashmap! { commit_id1 => vec![(0..3, 0..3), (6..9, 6..9)] }
);
}
#[test]
fn test_split_file_hunks_contiguous_ranges_insert() {
let commit_id1 = &CommitId::from_hex("111111");
let commit_id2 = &CommitId::from_hex("222222");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1X\n1a\n1b\n2a\n2b\n"])
),
hashmap! { commit_id1 => vec![(0..0, 0..3)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1X\n1b\n2a\n2b\n"])
),
hashmap! { commit_id1 => vec![(3..3, 3..6)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n3X\n2a\n2b\n"])
),
hashmap! {}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n2a\n2X\n2b\n"])
),
hashmap! { commit_id2 => vec![(9..9, 9..12)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n2a\n2b\n2X\n"])
),
hashmap! { commit_id2 => vec![(12..12, 12..15)] }
);
}
#[test]
fn test_split_file_hunks_contiguous_ranges_delete() {
let commit_id1 = &CommitId::from_hex("111111");
let commit_id2 = &CommitId::from_hex("222222");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1b\n2a\n2b\n"])
),
hashmap! { commit_id1 => vec![(0..3, 0..0)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1a\n2a\n2b\n"])
),
hashmap! { commit_id1 => vec![(3..6, 3..3)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n2b\n"])
),
hashmap! { commit_id2 => vec![(6..9, 6..6)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n2a\n"])
),
hashmap! { commit_id2 => vec![(9..12, 9..9)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1b\n2a\n"])
),
hashmap! {
commit_id1 => vec![(0..3, 0..0)],
commit_id2 => vec![(9..12, 6..6)],
}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1a\n"])
),
hashmap! {
commit_id1 => vec![(3..6, 3..3)],
commit_id2 => vec![(6..12, 3..3)],
}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1a\n2b\n"])
),
hashmap! {
commit_id1 => vec![(3..6, 3..3)],
commit_id2 => vec![(6..9, 3..3)],
}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "2b\n"])
),
hashmap! {
commit_id1 => vec![(0..6, 0..0)],
commit_id2 => vec![(6..9, 0..0)],
}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", ""])
),
hashmap! {
commit_id1 => vec![(0..6, 0..0)],
commit_id2 => vec![(6..12, 0..0)],
}
);
}
#[test]
fn test_split_file_hunks_contiguous_ranges_modify() {
let commit_id1 = &CommitId::from_hex("111111");
let commit_id2 = &CommitId::from_hex("222222");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1A\n1b\n2a\n2b\n"])
),
hashmap! { commit_id1 => vec![(0..3, 0..3)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1B\n2a\n2b\n"])
),
hashmap! { commit_id1 => vec![(3..6, 3..6)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1B\n2A\n2b\n"])
),
hashmap! {}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n2A\n2b\n"])
),
hashmap! { commit_id2 => vec![(6..9, 6..9)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n2a\n2B\n"])
),
hashmap! { commit_id2 => vec![(9..12, 9..12)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1A\n1b\n2a\n2B\n"])
),
hashmap! {
commit_id1 => vec![(0..3, 0..3)],
commit_id2 => vec![(9..12, 9..12)],
}
);
}
#[test]
fn test_split_file_hunks_contiguous_ranges_modify_insert() {
let commit_id1 = &CommitId::from_hex("111111");
let commit_id2 = &CommitId::from_hex("222222");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1A\n1B\n1X\n2a\n2b\n"])
),
hashmap! { commit_id1 => vec![(0..6, 0..9)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n2X\n2A\n2B\n"])
),
hashmap! { commit_id2 => vec![(6..12, 6..15)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n2A\n2B\n2X\n"])
),
hashmap! { commit_id2 => vec![(6..12, 6..15)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1A\n1b\n3X\n2a\n2B\n"])
),
hashmap! {
commit_id1 => vec![(0..3, 0..3)],
commit_id2 => vec![(9..12, 12..15)],
}
);
}
#[test]
fn test_split_file_hunks_contiguous_ranges_modify_delete() {
let commit_id1 = &CommitId::from_hex("111111");
let commit_id2 = &CommitId::from_hex("222222");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1A\n2a\n2b\n"])
),
hashmap! { commit_id1 => vec![(0..6, 0..3)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n2B\n"])
),
hashmap! { commit_id2 => vec![(6..12, 6..9)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1A\n2a\n2B\n"])
),
hashmap! {
commit_id1 => vec![(0..6, 0..3)],
commit_id2 => vec![(9..12, 6..9)],
}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1A\n1b\n2B\n"])
),
hashmap! {
commit_id1 => vec![(0..3, 0..3)],
commit_id2 => vec![(6..12, 6..9)],
}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 6..12)],
&Diff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1B\n2b\n"])
),
hashmap! {}
);
}
#[test]
fn test_split_file_hunks_non_contiguous_ranges_insert() {
let commit_id1 = &CommitId::from_hex("111111");
let commit_id2 = &CommitId::from_hex("222222");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n1X\n0a\n2a\n2b\n"])
),
hashmap! { commit_id1 => vec![(6..6, 6..9)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n0a\n2X\n2a\n2b\n"])
),
hashmap! { commit_id2 => vec![(9..9, 9..12)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n1X\n0a\n2X\n2a\n2b\n"])
),
hashmap! {
commit_id1 => vec![(6..6, 6..9)],
commit_id2 => vec![(9..9, 12..15)],
}
);
}
#[test]
fn test_split_file_hunks_non_contiguous_ranges_insert_modify_masked() {
let commit_id1 = &CommitId::from_hex("111111");
let commit_id2 = &CommitId::from_hex("222222");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n1X\n0A\n2a\n2b\n"])
),
hashmap! {}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n0A\n2X\n2a\n2b\n"])
),
hashmap! {}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n1X\n0A\n2X\n2a\n2b\n"])
),
hashmap! {}
);
}
#[test]
fn test_split_file_hunks_non_contiguous_ranges_delete() {
let commit_id1 = &CommitId::from_hex("111111");
let commit_id2 = &CommitId::from_hex("222222");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n0a\n2a\n2b\n"])
),
hashmap! { commit_id1 => vec![(3..6, 3..3)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n0a\n2b\n"])
),
hashmap! { commit_id2 => vec![(9..12, 9..9)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n0a\n2b\n"])
),
hashmap! {
commit_id1 => vec![(3..6, 3..3)],
commit_id2 => vec![(9..12, 6..6)],
}
);
}
#[test]
fn test_split_file_hunks_non_contiguous_ranges_delete_modify_masked() {
let commit_id1 = &CommitId::from_hex("111111");
let commit_id2 = &CommitId::from_hex("222222");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n0A\n2a\n2b\n"])
),
hashmap! {}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n0A\n2b\n"])
),
hashmap! {}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n0A\n2b\n"])
),
hashmap! {}
);
}
#[test]
fn test_split_file_hunks_non_contiguous_ranges_delete_delete_masked() {
let commit_id1 = &CommitId::from_hex("111111");
let commit_id2 = &CommitId::from_hex("222222");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n2a\n2b\n"])
),
hashmap! {}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n2b\n"])
),
hashmap! {}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n2b\n"])
),
hashmap! {}
);
}
#[test]
fn test_split_file_hunks_non_contiguous_ranges_modify() {
let commit_id1 = &CommitId::from_hex("111111");
let commit_id2 = &CommitId::from_hex("222222");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1B\n0a\n2a\n2b\n"])
),
hashmap! { commit_id1 => vec![(3..6, 3..6)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n0a\n2A\n2b\n"])
),
hashmap! { commit_id2 => vec![(9..12, 9..12)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1B\n0a\n2A\n2b\n"])
),
hashmap! {
commit_id1 => vec![(3..6, 3..6)],
commit_id2 => vec![(9..12, 9..12)],
}
);
}
#[test]
fn test_split_file_hunks_non_contiguous_ranges_modify_modify_masked() {
let commit_id1 = &CommitId::from_hex("111111");
let commit_id2 = &CommitId::from_hex("222222");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1B\n0A\n2a\n2b\n"])
),
hashmap! {}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n0A\n2A\n2b\n"])
),
hashmap! {}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6), (commit_id2, 9..15)],
&Diff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1B\n0A\n2A\n2b\n"])
),
hashmap! {}
);
}
#[test]
fn test_split_file_hunks_non_contiguous_tail_range_insert() {
let commit_id1 = &CommitId::from_hex("111111");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6) ],
&Diff::by_line(["1a\n1b\n0a\n", "1a\n1b\n1X\n0a\n"])
),
hashmap! { commit_id1 => vec![(6..6, 6..9)] }
);
}
#[test]
fn test_split_file_hunks_non_contiguous_tail_range_insert_modify_masked() {
let commit_id1 = &CommitId::from_hex("111111");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6) ],
&Diff::by_line(["1a\n1b\n0a\n", "1a\n1b\n1X\n0A\n"])
),
hashmap! {}
);
}
#[test]
fn test_split_file_hunks_non_contiguous_tail_range_delete() {
let commit_id1 = &CommitId::from_hex("111111");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6) ],
&Diff::by_line(["1a\n1b\n0a\n", "1a\n0a\n"])
),
hashmap! { commit_id1 => vec![(3..6, 3..3)] }
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6) ],
&Diff::by_line(["1a\n1b\n0a\n", "0a\n"])
),
hashmap! { commit_id1 => vec![(0..6, 0..0)] }
);
}
#[test]
fn test_split_file_hunks_non_contiguous_tail_range_delete_modify_masked() {
let commit_id1 = &CommitId::from_hex("111111");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6) ],
&Diff::by_line(["1a\n1b\n0a\n", "1a\n0A\n"])
),
hashmap! {}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6) ],
&Diff::by_line(["1a\n1b\n0a\n", "0A\n"])
),
hashmap! {}
);
}
#[test]
fn test_split_file_hunks_non_contiguous_tail_range_delete_delete_masked() {
let commit_id1 = &CommitId::from_hex("111111");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6) ],
&Diff::by_line(["1a\n1b\n0a\n", "1a\n"])
),
hashmap! {}
);
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6) ],
&Diff::by_line(["1a\n1b\n0a\n", ""])
),
hashmap! {}
);
}
#[test]
fn test_split_file_hunks_non_contiguous_tail_range_modify() {
let commit_id1 = &CommitId::from_hex("111111");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6) ],
&Diff::by_line(["1a\n1b\n0a\n", "1a\n1B\n0a\n"])
),
hashmap! { commit_id1 => vec![(3..6, 3..6)] }
);
}
#[test]
fn test_split_file_hunks_non_contiguous_tail_range_modify_modify_masked() {
let commit_id1 = &CommitId::from_hex("111111");
assert_eq!(
split_file_hunks(
&[(commit_id1, 0..6) ],
&Diff::by_line(["1a\n1b\n0a\n", "1a\n1B\n0A\n"])
),
hashmap! {}
);
}
#[test]
fn test_split_file_hunks_multiple_edits() {
let commit_id1 = &CommitId::from_hex("111111");
let commit_id2 = &CommitId::from_hex("222222");
let commit_id3 = &CommitId::from_hex("333333");
assert_eq!(
split_file_hunks(
&[
(commit_id1, 0..3), (commit_id2, 3..6), (commit_id1, 6..15), (commit_id3, 15..21), ],
&Diff::by_line([
"1a\n2a\n1b\n1c\n1d\n3a\n3b\n",
"1A\n2a\n1B\n1d\n3X\n3A\n3b\n3Y\n"
])
),
hashmap! {
commit_id1 => vec![(0..3, 0..3), (6..12, 6..9)],
commit_id3 => vec![(15..18, 12..18), (21..21, 21..24)],
}
);
}
#[test]
fn test_combine_texts() {
assert_eq!(combine_texts(b"", b"", &[]), "");
assert_eq!(combine_texts(b"foo", b"bar", &[]), "foo");
assert_eq!(combine_texts(b"foo", b"bar", &[(0..3, 0..3)]), "bar");
assert_eq!(
combine_texts(
b"1a\n2a\n1b\n1c\n1d\n3a\n3b\n",
b"1A\n2a\n1B\n1d\n3X\n3A\n3b\n3Y\n",
&[(0..3, 0..3), (6..12, 6..9)]
),
"1A\n2a\n1B\n1d\n3a\n3b\n"
);
assert_eq!(
combine_texts(
b"1a\n2a\n1b\n1c\n1d\n3a\n3b\n",
b"1A\n2a\n1B\n1d\n3X\n3A\n3b\n3Y\n",
&[(15..18, 12..18), (21..21, 21..24)]
),
"1a\n2a\n1b\n1c\n1d\n3X\n3A\n3b\n3Y\n"
);
}
}