1use std::cmp;
19use std::collections::HashMap;
20use std::ops::Range;
21use std::sync::Arc;
22
23use bstr::BString;
24use futures::StreamExt as _;
25use itertools::Itertools as _;
26use thiserror::Error;
27
28use crate::annotate::FileAnnotator;
29use crate::backend::BackendError;
30use crate::backend::BackendResult;
31use crate::backend::CommitId;
32use crate::backend::TreeValue;
33use crate::commit::Commit;
34use crate::commit::conflict_label_for_commits;
35use crate::conflicts::MaterializedFileValue;
36use crate::conflicts::MaterializedTreeValue;
37use crate::conflicts::materialized_diff_stream;
38use crate::copies::CopyRecords;
39use crate::diff::ContentDiff;
40use crate::diff::DiffHunkKind;
41use crate::matchers::Matcher;
42use crate::merge::Diff;
43use crate::merge::Merge;
44use crate::merged_tree::MergedTree;
45use crate::merged_tree_builder::MergedTreeBuilder;
46use crate::repo::MutableRepo;
47use crate::repo::Repo;
48use crate::repo_path::RepoPathBuf;
49use crate::revset::ResolvedRevsetExpression;
50use crate::revset::RevsetEvaluationError;
51
52#[derive(Clone, Debug)]
54pub struct AbsorbSource {
55 commit: Commit,
56 parents: Vec<Commit>,
57 parent_tree: MergedTree,
58 source_tree: MergedTree,
59}
60
61impl AbsorbSource {
62 pub async fn from_commit(repo: &dyn Repo, commit: Commit) -> BackendResult<Self> {
64 let parent_tree = commit.parent_tree(repo).await?;
65 let source_tree = commit.tree();
66 Self::from_tree(commit, source_tree, parent_tree).await
67 }
68
69 pub async fn from_tree(
71 commit: Commit,
72 source_tree: MergedTree,
73 parent_tree: MergedTree,
74 ) -> BackendResult<Self> {
75 let parents = commit.parents().await?;
76 Ok(Self {
77 commit,
78 parents,
79 parent_tree,
80 source_tree,
81 })
82 }
83}
84
85#[derive(Debug, Error)]
87pub enum AbsorbError {
88 #[error(transparent)]
90 Backend(#[from] BackendError),
91 #[error(transparent)]
93 RevsetEvaluation(#[from] RevsetEvaluationError),
94}
95
96#[derive(Default)]
99pub struct SelectedTrees {
100 pub target_commits: HashMap<CommitId, MergedTreeBuilder>,
102 pub skipped_paths: Vec<(RepoPathBuf, String)>,
104}
105
106pub async fn split_hunks_to_trees(
109 repo: &dyn Repo,
110 source: &AbsorbSource,
111 destinations: &Arc<ResolvedRevsetExpression>,
112 matcher: &dyn Matcher,
113) -> Result<SelectedTrees, AbsorbError> {
114 let mut selected_trees = SelectedTrees::default();
115
116 let left_tree = &source.parent_tree;
117 let right_tree = &source.source_tree;
118 let copy_records = CopyRecords::default();
120 let tree_diff = left_tree.diff_stream_with_copies(right_tree, matcher, ©_records);
121 let mut diff_stream = materialized_diff_stream(
122 repo.store(),
123 tree_diff,
124 Diff::new(left_tree.labels(), right_tree.labels()),
125 );
126 while let Some(entry) = diff_stream.next().await {
127 let left_path = entry.path.source();
128 let right_path = entry.path.target();
129 let values = entry.values?;
130 let (left_text, executable, copy_id) = match to_file_value(values.before) {
131 Ok(Some(mut value)) => (
132 value.read_all(left_path).await?,
133 value.executable,
134 value.copy_id,
135 ),
136 Ok(None) => continue,
138 Err(reason) => {
139 selected_trees
140 .skipped_paths
141 .push((left_path.to_owned(), reason));
142 continue;
143 }
144 };
145 let (right_text, deleted) = match to_file_value(values.after) {
146 Ok(Some(mut value)) => (value.read_all(right_path).await?, false),
147 Ok(None) => (vec![], true),
148 Err(reason) => {
149 selected_trees
150 .skipped_paths
151 .push((right_path.to_owned(), reason));
152 continue;
153 }
154 };
155
156 let mut annotator =
158 FileAnnotator::with_file_content(source.commit.id(), left_path, left_text.clone());
159 annotator.compute(repo, destinations).await?;
160 let annotation = annotator.to_annotation();
161 let annotation_ranges = annotation
162 .compact_line_ranges()
163 .filter_map(|(commit_id, range)| Some((commit_id.ok()?, range)))
164 .collect_vec();
165 let diff = ContentDiff::by_line([&left_text, &right_text]);
166 let selected_ranges = split_file_hunks(&annotation_ranges, &diff);
167 for (&commit_id, ranges) in &selected_ranges {
169 let tree_builder = selected_trees
170 .target_commits
171 .entry(commit_id.clone())
172 .or_insert_with(|| MergedTreeBuilder::new(left_tree.clone()));
173 let new_text = combine_texts(&left_text, &right_text, ranges);
174 let new_tree_value = if new_text.is_empty() && deleted {
178 Merge::absent()
179 } else {
180 let id = repo
181 .store()
182 .write_file(left_path, &mut new_text.as_slice())
183 .await?;
184 Merge::normal(TreeValue::File {
185 id,
186 executable,
187 copy_id: copy_id.clone(),
188 })
189 };
190 tree_builder.set_or_remove(left_path.to_owned(), new_tree_value);
191 }
192 }
193
194 Ok(selected_trees)
195}
196
197type SelectedRange = (Range<usize>, Range<usize>);
198
199fn split_file_hunks<'a>(
202 mut annotation_ranges: &[(&'a CommitId, Range<usize>)],
203 diff: &ContentDiff,
204) -> HashMap<&'a CommitId, Vec<SelectedRange>> {
205 debug_assert!(annotation_ranges.iter().all(|(_, range)| !range.is_empty()));
206 let mut selected_ranges: HashMap<&CommitId, Vec<_>> = HashMap::new();
207 let mut diff_hunk_ranges = diff
208 .hunk_ranges()
209 .filter(|hunk| hunk.kind == DiffHunkKind::Different);
210 while !annotation_ranges.is_empty() {
211 let Some(hunk) = diff_hunk_ranges.next() else {
212 break;
213 };
214 let [left_range, right_range]: &[_; 2] = hunk.ranges[..].try_into().unwrap();
215 assert!(!left_range.is_empty() || !right_range.is_empty());
216 if right_range.is_empty() {
217 let skip = annotation_ranges
220 .iter()
221 .take_while(|(_, range)| range.end <= left_range.start)
222 .count();
223 annotation_ranges = &annotation_ranges[skip..];
224 let pre_overlap = annotation_ranges
225 .iter()
226 .take_while(|(_, range)| range.end < left_range.end)
227 .count();
228 let maybe_overlapped_ranges = annotation_ranges.get(..pre_overlap + 1);
229 annotation_ranges = &annotation_ranges[pre_overlap..];
230 let Some(overlapped_ranges) = maybe_overlapped_ranges else {
231 continue;
232 };
233 let all_covered = overlapped_ranges
235 .iter()
236 .try_fold(left_range.start, |prev_end, (_, cur)| {
237 (cur.start <= prev_end).then_some(cur.end)
238 })
239 .inspect(|&last_end| assert!(left_range.end <= last_end))
240 .is_some();
241 if all_covered {
242 for (commit_id, cur_range) in overlapped_ranges {
243 let start = cmp::max(cur_range.start, left_range.start);
244 let end = cmp::min(cur_range.end, left_range.end);
245 assert!(start < end);
246 let selected = selected_ranges.entry(commit_id).or_default();
247 selected.push((start..end, right_range.clone()));
248 }
249 }
250 } else {
251 let skip = annotation_ranges
254 .iter()
255 .take_while(|(_, range)| range.end < left_range.end)
256 .count();
257 annotation_ranges = &annotation_ranges[skip..];
258 let Some((commit_id, cur_range)) = annotation_ranges.first() else {
259 continue;
260 };
261 let contained = cur_range.start <= left_range.start && left_range.end <= cur_range.end;
262 let ambiguous = cur_range.end == left_range.start
265 && annotation_ranges
266 .get(1)
267 .is_some_and(|(_, next_range)| next_range.start == left_range.end);
268 if contained && !ambiguous {
269 let selected = selected_ranges.entry(commit_id).or_default();
270 selected.push((left_range.clone(), right_range.clone()));
271 }
272 }
273 }
274 selected_ranges
275}
276
277fn combine_texts(text1: &[u8], text2: &[u8], selected_ranges: &[SelectedRange]) -> BString {
280 itertools::chain!(
281 [(0..0, 0..0)],
282 selected_ranges.iter().cloned(),
283 [(text1.len()..text1.len(), text2.len()..text2.len())],
284 )
285 .tuple_windows()
286 .map(|((prev1, _), (cur1, cur2))| (prev1.end..cur1.start, cur2))
288 .flat_map(|(range1, range2)| [&text1[range1], &text2[range2]])
289 .collect()
290}
291
292#[derive(Clone, Debug)]
294pub struct AbsorbStats {
295 pub rewritten_source: Option<Commit>,
298 pub rewritten_destinations: Vec<Commit>,
301 pub num_rebased: usize,
304}
305
306pub async fn absorb_hunks(
309 repo: &mut MutableRepo,
310 source: &AbsorbSource,
311 mut selected_trees: HashMap<CommitId, MergedTreeBuilder>,
312) -> BackendResult<AbsorbStats> {
313 let mut rewritten_source = None;
314 let mut rewritten_destinations = Vec::new();
315 let mut num_rebased = 0;
316 let parents_label = conflict_label_for_commits(&source.parents);
317 let source_commit_label = source.commit.conflict_label();
318 repo.transform_descendants(selected_trees.keys().cloned().collect(), async |rewriter| {
321 if rewriter.old_commit().id() == source.commit.id() {
323 let commit_builder = rewriter.reparent();
324 if commit_builder.is_discardable().await? {
325 commit_builder.abandon();
326 } else {
327 rewritten_source = Some(commit_builder.write().await?);
328 num_rebased += 1;
329 }
330 return Ok(());
331 }
332 let Some(tree_builder) = selected_trees.remove(rewriter.old_commit().id()) else {
333 rewriter.rebase().await?.write().await?;
334 num_rebased += 1;
335 return Ok(());
336 };
337 let selected_tree = tree_builder.write_tree().await?;
339 let destination_label = rewriter.old_commit().conflict_label();
340 let commit_builder = rewriter.rebase().await?;
341 let destination_tree = commit_builder.tree();
342 let new_tree = MergedTree::merge(Merge::from_vec(vec![
343 (
344 destination_tree,
345 format!("{destination_label} (absorb destination)"),
346 ),
347 (
348 source.parent_tree.clone(),
349 format!("{parents_label} (parents of absorbed revision)"),
350 ),
351 (
352 selected_tree,
353 format!("absorbed changes (from {source_commit_label})"),
354 ),
355 ]))
356 .await?;
357 let mut predecessors = commit_builder.predecessors().to_vec();
358 predecessors.push(source.commit.id().clone());
359 let new_commit = commit_builder
360 .set_tree(new_tree)
361 .set_predecessors(predecessors)
362 .write()
363 .await?;
364 rewritten_destinations.push(new_commit);
365 Ok(())
366 })
367 .await?;
368 Ok(AbsorbStats {
369 rewritten_source,
370 rewritten_destinations,
371 num_rebased,
372 })
373}
374
375fn to_file_value(value: MaterializedTreeValue) -> Result<Option<MaterializedFileValue>, String> {
376 match value {
377 MaterializedTreeValue::Absent => Ok(None), MaterializedTreeValue::AccessDenied(err) => Err(format!("Access is denied: {err}")),
379 MaterializedTreeValue::File(file) => Ok(Some(file)),
380 MaterializedTreeValue::Symlink { .. } => Err("Is a symlink".into()),
381 MaterializedTreeValue::FileConflict(_) | MaterializedTreeValue::OtherConflict { .. } => {
382 Err("Is a conflict".into())
383 }
384 MaterializedTreeValue::GitSubmodule(_) => Err("Is a Git submodule".into()),
385 MaterializedTreeValue::Tree(_) => panic!("diff should not contain trees"),
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use maplit::hashmap;
392
393 use super::*;
394
395 #[test]
396 fn test_split_file_hunks_empty_or_single_line() {
397 let commit_id1 = &CommitId::from_hex("111111");
398
399 assert_eq!(
401 split_file_hunks(&[], &ContentDiff::by_line(["", ""])),
402 hashmap! {}
403 );
404
405 assert_eq!(
407 split_file_hunks(&[], &ContentDiff::by_line(["", "2X\n"])),
408 hashmap! {}
409 );
410 assert_eq!(
412 split_file_hunks(&[(commit_id1, 0..3)], &ContentDiff::by_line(["1a\n", ""])),
413 hashmap! { commit_id1 => vec![(0..3, 0..0)] }
414 );
415 assert_eq!(
417 split_file_hunks(
418 &[(commit_id1, 0..3)],
419 &ContentDiff::by_line(["1a\n", "1AA\n"])
420 ),
421 hashmap! { commit_id1 => vec![(0..3, 0..4)] }
422 );
423 }
424
425 #[test]
426 fn test_split_file_hunks_single_range() {
427 let commit_id1 = &CommitId::from_hex("111111");
428
429 assert_eq!(
431 split_file_hunks(
432 &[(commit_id1, 0..6)],
433 &ContentDiff::by_line(["1a\n1b\n", "1X\n1a\n1Y\n1b\n1Z\n"])
434 ),
435 hashmap! {
436 commit_id1 => vec![(0..0, 0..3), (3..3, 6..9), (6..6, 12..15)],
437 }
438 );
439 assert_eq!(
441 split_file_hunks(
442 &[(commit_id1, 0..15)],
443 &ContentDiff::by_line(["1a\n1b\n1c\n1d\n1e\n1f\n", "1b\n1d\n1f\n"])
444 ),
445 hashmap! {
446 commit_id1 => vec![(0..3, 0..0), (6..9, 3..3), (12..15, 6..6)],
447 }
448 );
449 assert_eq!(
451 split_file_hunks(
452 &[(commit_id1, 0..12)],
453 &ContentDiff::by_line(["1a\n1b\n1c\n1d\n", "1A\n1b\n1C\n1d\n"])
454 ),
455 hashmap! { commit_id1 => vec![(0..3, 0..3), (6..9, 6..9)] }
456 );
457 }
458
459 #[test]
460 fn test_split_file_hunks_contiguous_ranges_insert() {
461 let commit_id1 = &CommitId::from_hex("111111");
462 let commit_id2 = &CommitId::from_hex("222222");
463
464 assert_eq!(
466 split_file_hunks(
467 &[(commit_id1, 0..6), (commit_id2, 6..12)],
468 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1X\n1a\n1b\n2a\n2b\n"])
469 ),
470 hashmap! { commit_id1 => vec![(0..0, 0..3)] }
471 );
472 assert_eq!(
474 split_file_hunks(
475 &[(commit_id1, 0..6), (commit_id2, 6..12)],
476 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1X\n1b\n2a\n2b\n"])
477 ),
478 hashmap! { commit_id1 => vec![(3..3, 3..6)] }
479 );
480 assert_eq!(
482 split_file_hunks(
483 &[(commit_id1, 0..6), (commit_id2, 6..12)],
484 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n3X\n2a\n2b\n"])
485 ),
486 hashmap! {}
487 );
488 assert_eq!(
490 split_file_hunks(
491 &[(commit_id1, 0..6), (commit_id2, 6..12)],
492 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n2a\n2X\n2b\n"])
493 ),
494 hashmap! { commit_id2 => vec![(9..9, 9..12)] }
495 );
496 assert_eq!(
498 split_file_hunks(
499 &[(commit_id1, 0..6), (commit_id2, 6..12)],
500 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n2a\n2b\n2X\n"])
501 ),
502 hashmap! { commit_id2 => vec![(12..12, 12..15)] }
503 );
504 }
505
506 #[test]
507 fn test_split_file_hunks_contiguous_ranges_delete() {
508 let commit_id1 = &CommitId::from_hex("111111");
509 let commit_id2 = &CommitId::from_hex("222222");
510
511 assert_eq!(
513 split_file_hunks(
514 &[(commit_id1, 0..6), (commit_id2, 6..12)],
515 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1b\n2a\n2b\n"])
516 ),
517 hashmap! { commit_id1 => vec![(0..3, 0..0)] }
518 );
519 assert_eq!(
521 split_file_hunks(
522 &[(commit_id1, 0..6), (commit_id2, 6..12)],
523 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1a\n2a\n2b\n"])
524 ),
525 hashmap! { commit_id1 => vec![(3..6, 3..3)] }
526 );
527 assert_eq!(
529 split_file_hunks(
530 &[(commit_id1, 0..6), (commit_id2, 6..12)],
531 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n2b\n"])
532 ),
533 hashmap! { commit_id2 => vec![(6..9, 6..6)] }
534 );
535 assert_eq!(
537 split_file_hunks(
538 &[(commit_id1, 0..6), (commit_id2, 6..12)],
539 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n2a\n"])
540 ),
541 hashmap! { commit_id2 => vec![(9..12, 9..9)] }
542 );
543 assert_eq!(
545 split_file_hunks(
546 &[(commit_id1, 0..6), (commit_id2, 6..12)],
547 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1b\n2a\n"])
548 ),
549 hashmap! {
550 commit_id1 => vec![(0..3, 0..0)],
551 commit_id2 => vec![(9..12, 6..6)],
552 }
553 );
554
555 assert_eq!(
557 split_file_hunks(
558 &[(commit_id1, 0..6), (commit_id2, 6..12)],
559 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1a\n"])
560 ),
561 hashmap! {
562 commit_id1 => vec![(3..6, 3..3)],
563 commit_id2 => vec![(6..12, 3..3)],
564 }
565 );
566 assert_eq!(
568 split_file_hunks(
569 &[(commit_id1, 0..6), (commit_id2, 6..12)],
570 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1a\n2b\n"])
571 ),
572 hashmap! {
573 commit_id1 => vec![(3..6, 3..3)],
574 commit_id2 => vec![(6..9, 3..3)],
575 }
576 );
577 assert_eq!(
579 split_file_hunks(
580 &[(commit_id1, 0..6), (commit_id2, 6..12)],
581 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "2b\n"])
582 ),
583 hashmap! {
584 commit_id1 => vec![(0..6, 0..0)],
585 commit_id2 => vec![(6..9, 0..0)],
586 }
587 );
588
589 assert_eq!(
591 split_file_hunks(
592 &[(commit_id1, 0..6), (commit_id2, 6..12)],
593 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", ""])
594 ),
595 hashmap! {
596 commit_id1 => vec![(0..6, 0..0)],
597 commit_id2 => vec![(6..12, 0..0)],
598 }
599 );
600 }
601
602 #[test]
603 fn test_split_file_hunks_contiguous_ranges_modify() {
604 let commit_id1 = &CommitId::from_hex("111111");
605 let commit_id2 = &CommitId::from_hex("222222");
606
607 assert_eq!(
609 split_file_hunks(
610 &[(commit_id1, 0..6), (commit_id2, 6..12)],
611 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1A\n1b\n2a\n2b\n"])
612 ),
613 hashmap! { commit_id1 => vec![(0..3, 0..3)] }
614 );
615 assert_eq!(
617 split_file_hunks(
618 &[(commit_id1, 0..6), (commit_id2, 6..12)],
619 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1B\n2a\n2b\n"])
620 ),
621 hashmap! { commit_id1 => vec![(3..6, 3..6)] }
622 );
623 assert_eq!(
626 split_file_hunks(
627 &[(commit_id1, 0..6), (commit_id2, 6..12)],
628 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1B\n2A\n2b\n"])
629 ),
630 hashmap! {}
631 );
632 assert_eq!(
634 split_file_hunks(
635 &[(commit_id1, 0..6), (commit_id2, 6..12)],
636 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n2A\n2b\n"])
637 ),
638 hashmap! { commit_id2 => vec![(6..9, 6..9)] }
639 );
640 assert_eq!(
642 split_file_hunks(
643 &[(commit_id1, 0..6), (commit_id2, 6..12)],
644 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n2a\n2B\n"])
645 ),
646 hashmap! { commit_id2 => vec![(9..12, 9..12)] }
647 );
648 assert_eq!(
650 split_file_hunks(
651 &[(commit_id1, 0..6), (commit_id2, 6..12)],
652 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1A\n1b\n2a\n2B\n"])
653 ),
654 hashmap! {
655 commit_id1 => vec![(0..3, 0..3)],
656 commit_id2 => vec![(9..12, 9..12)],
657 }
658 );
659 }
660
661 #[test]
662 fn test_split_file_hunks_contiguous_ranges_modify_insert() {
663 let commit_id1 = &CommitId::from_hex("111111");
664 let commit_id2 = &CommitId::from_hex("222222");
665
666 assert_eq!(
668 split_file_hunks(
669 &[(commit_id1, 0..6), (commit_id2, 6..12)],
670 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1A\n1B\n1X\n2a\n2b\n"])
671 ),
672 hashmap! { commit_id1 => vec![(0..6, 0..9)] }
673 );
674 assert_eq!(
676 split_file_hunks(
677 &[(commit_id1, 0..6), (commit_id2, 6..12)],
678 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n2X\n2A\n2B\n"])
679 ),
680 hashmap! { commit_id2 => vec![(6..12, 6..15)] }
681 );
682 assert_eq!(
684 split_file_hunks(
685 &[(commit_id1, 0..6), (commit_id2, 6..12)],
686 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n2A\n2B\n2X\n"])
687 ),
688 hashmap! { commit_id2 => vec![(6..12, 6..15)] }
689 );
690 assert_eq!(
693 split_file_hunks(
694 &[(commit_id1, 0..6), (commit_id2, 6..12)],
695 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1A\n1b\n3X\n2a\n2B\n"])
696 ),
697 hashmap! {
698 commit_id1 => vec![(0..3, 0..3)],
699 commit_id2 => vec![(9..12, 12..15)],
700 }
701 );
702 }
703
704 #[test]
705 fn test_split_file_hunks_contiguous_ranges_modify_delete() {
706 let commit_id1 = &CommitId::from_hex("111111");
707 let commit_id2 = &CommitId::from_hex("222222");
708
709 assert_eq!(
711 split_file_hunks(
712 &[(commit_id1, 0..6), (commit_id2, 6..12)],
713 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1A\n2a\n2b\n"])
714 ),
715 hashmap! { commit_id1 => vec![(0..6, 0..3)] }
716 );
717 assert_eq!(
719 split_file_hunks(
720 &[(commit_id1, 0..6), (commit_id2, 6..12)],
721 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1b\n2B\n"])
722 ),
723 hashmap! { commit_id2 => vec![(6..12, 6..9)] }
724 );
725 assert_eq!(
727 split_file_hunks(
728 &[(commit_id1, 0..6), (commit_id2, 6..12)],
729 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1A\n2a\n2B\n"])
730 ),
731 hashmap! {
732 commit_id1 => vec![(0..6, 0..3)],
733 commit_id2 => vec![(9..12, 6..9)],
734 }
735 );
736 assert_eq!(
738 split_file_hunks(
739 &[(commit_id1, 0..6), (commit_id2, 6..12)],
740 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1A\n1b\n2B\n"])
741 ),
742 hashmap! {
743 commit_id1 => vec![(0..3, 0..3)],
744 commit_id2 => vec![(6..12, 6..9)],
745 }
746 );
747 assert_eq!(
749 split_file_hunks(
750 &[(commit_id1, 0..6), (commit_id2, 6..12)],
751 &ContentDiff::by_line(["1a\n1b\n2a\n2b\n", "1a\n1B\n2b\n"])
752 ),
753 hashmap! {}
754 );
755 }
756
757 #[test]
758 fn test_split_file_hunks_non_contiguous_ranges_insert() {
759 let commit_id1 = &CommitId::from_hex("111111");
760 let commit_id2 = &CommitId::from_hex("222222");
761
762 assert_eq!(
764 split_file_hunks(
765 &[(commit_id1, 0..6), (commit_id2, 9..15)],
766 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n1X\n0a\n2a\n2b\n"])
767 ),
768 hashmap! { commit_id1 => vec![(6..6, 6..9)] }
769 );
770 assert_eq!(
772 split_file_hunks(
773 &[(commit_id1, 0..6), (commit_id2, 9..15)],
774 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n0a\n2X\n2a\n2b\n"])
775 ),
776 hashmap! { commit_id2 => vec![(9..9, 9..12)] }
777 );
778 assert_eq!(
780 split_file_hunks(
781 &[(commit_id1, 0..6), (commit_id2, 9..15)],
782 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n1X\n0a\n2X\n2a\n2b\n"])
783 ),
784 hashmap! {
785 commit_id1 => vec![(6..6, 6..9)],
786 commit_id2 => vec![(9..9, 12..15)],
787 }
788 );
789 }
790
791 #[test]
792 fn test_split_file_hunks_non_contiguous_ranges_insert_modify_masked() {
793 let commit_id1 = &CommitId::from_hex("111111");
794 let commit_id2 = &CommitId::from_hex("222222");
795
796 assert_eq!(
798 split_file_hunks(
799 &[(commit_id1, 0..6), (commit_id2, 9..15)],
800 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n1X\n0A\n2a\n2b\n"])
801 ),
802 hashmap! {}
803 );
804 assert_eq!(
806 split_file_hunks(
807 &[(commit_id1, 0..6), (commit_id2, 9..15)],
808 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n0A\n2X\n2a\n2b\n"])
809 ),
810 hashmap! {}
811 );
812 assert_eq!(
814 split_file_hunks(
815 &[(commit_id1, 0..6), (commit_id2, 9..15)],
816 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n1X\n0A\n2X\n2a\n2b\n"])
817 ),
818 hashmap! {}
819 );
820 }
821
822 #[test]
823 fn test_split_file_hunks_non_contiguous_ranges_delete() {
824 let commit_id1 = &CommitId::from_hex("111111");
825 let commit_id2 = &CommitId::from_hex("222222");
826
827 assert_eq!(
829 split_file_hunks(
830 &[(commit_id1, 0..6), (commit_id2, 9..15)],
831 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n0a\n2a\n2b\n"])
832 ),
833 hashmap! { commit_id1 => vec![(3..6, 3..3)] }
834 );
835 assert_eq!(
837 split_file_hunks(
838 &[(commit_id1, 0..6), (commit_id2, 9..15)],
839 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n0a\n2b\n"])
840 ),
841 hashmap! { commit_id2 => vec![(9..12, 9..9)] }
842 );
843 assert_eq!(
845 split_file_hunks(
846 &[(commit_id1, 0..6), (commit_id2, 9..15)],
847 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n0a\n2b\n"])
848 ),
849 hashmap! {
850 commit_id1 => vec![(3..6, 3..3)],
851 commit_id2 => vec![(9..12, 6..6)],
852 }
853 );
854 }
855
856 #[test]
857 fn test_split_file_hunks_non_contiguous_ranges_delete_modify_masked() {
858 let commit_id1 = &CommitId::from_hex("111111");
859 let commit_id2 = &CommitId::from_hex("222222");
860
861 assert_eq!(
863 split_file_hunks(
864 &[(commit_id1, 0..6), (commit_id2, 9..15)],
865 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n0A\n2a\n2b\n"])
866 ),
867 hashmap! {}
868 );
869 assert_eq!(
871 split_file_hunks(
872 &[(commit_id1, 0..6), (commit_id2, 9..15)],
873 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n0A\n2b\n"])
874 ),
875 hashmap! {}
876 );
877 assert_eq!(
879 split_file_hunks(
880 &[(commit_id1, 0..6), (commit_id2, 9..15)],
881 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n0A\n2b\n"])
882 ),
883 hashmap! {}
884 );
885 }
886
887 #[test]
888 fn test_split_file_hunks_non_contiguous_ranges_delete_delete_masked() {
889 let commit_id1 = &CommitId::from_hex("111111");
890 let commit_id2 = &CommitId::from_hex("222222");
891
892 assert_eq!(
897 split_file_hunks(
898 &[(commit_id1, 0..6), (commit_id2, 9..15)],
899 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n2a\n2b\n"])
900 ),
901 hashmap! {}
902 );
903 assert_eq!(
905 split_file_hunks(
906 &[(commit_id1, 0..6), (commit_id2, 9..15)],
907 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n2b\n"])
908 ),
909 hashmap! {}
910 );
911 assert_eq!(
913 split_file_hunks(
914 &[(commit_id1, 0..6), (commit_id2, 9..15)],
915 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n2b\n"])
916 ),
917 hashmap! {}
918 );
919 }
920
921 #[test]
922 fn test_split_file_hunks_non_contiguous_ranges_modify() {
923 let commit_id1 = &CommitId::from_hex("111111");
924 let commit_id2 = &CommitId::from_hex("222222");
925
926 assert_eq!(
928 split_file_hunks(
929 &[(commit_id1, 0..6), (commit_id2, 9..15)],
930 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1B\n0a\n2a\n2b\n"])
931 ),
932 hashmap! { commit_id1 => vec![(3..6, 3..6)] }
933 );
934 assert_eq!(
936 split_file_hunks(
937 &[(commit_id1, 0..6), (commit_id2, 9..15)],
938 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n0a\n2A\n2b\n"])
939 ),
940 hashmap! { commit_id2 => vec![(9..12, 9..12)] }
941 );
942 assert_eq!(
944 split_file_hunks(
945 &[(commit_id1, 0..6), (commit_id2, 9..15)],
946 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1B\n0a\n2A\n2b\n"])
947 ),
948 hashmap! {
949 commit_id1 => vec![(3..6, 3..6)],
950 commit_id2 => vec![(9..12, 9..12)],
951 }
952 );
953 }
954
955 #[test]
956 fn test_split_file_hunks_non_contiguous_ranges_modify_modify_masked() {
957 let commit_id1 = &CommitId::from_hex("111111");
958 let commit_id2 = &CommitId::from_hex("222222");
959
960 assert_eq!(
962 split_file_hunks(
963 &[(commit_id1, 0..6), (commit_id2, 9..15)],
964 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1B\n0A\n2a\n2b\n"])
965 ),
966 hashmap! {}
967 );
968 assert_eq!(
970 split_file_hunks(
971 &[(commit_id1, 0..6), (commit_id2, 9..15)],
972 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1b\n0A\n2A\n2b\n"])
973 ),
974 hashmap! {}
975 );
976 assert_eq!(
978 split_file_hunks(
979 &[(commit_id1, 0..6), (commit_id2, 9..15)],
980 &ContentDiff::by_line(["1a\n1b\n0a\n2a\n2b\n", "1a\n1B\n0A\n2A\n2b\n"])
981 ),
982 hashmap! {}
983 );
984 }
985
986 #[test]
987 fn test_split_file_hunks_non_contiguous_tail_range_insert() {
988 let commit_id1 = &CommitId::from_hex("111111");
989
990 assert_eq!(
992 split_file_hunks(
993 &[(commit_id1, 0..6) ],
994 &ContentDiff::by_line(["1a\n1b\n0a\n", "1a\n1b\n1X\n0a\n"])
995 ),
996 hashmap! { commit_id1 => vec![(6..6, 6..9)] }
997 );
998 }
999
1000 #[test]
1001 fn test_split_file_hunks_non_contiguous_tail_range_insert_modify_masked() {
1002 let commit_id1 = &CommitId::from_hex("111111");
1003
1004 assert_eq!(
1006 split_file_hunks(
1007 &[(commit_id1, 0..6) ],
1008 &ContentDiff::by_line(["1a\n1b\n0a\n", "1a\n1b\n1X\n0A\n"])
1009 ),
1010 hashmap! {}
1011 );
1012 }
1013
1014 #[test]
1015 fn test_split_file_hunks_non_contiguous_tail_range_delete() {
1016 let commit_id1 = &CommitId::from_hex("111111");
1017
1018 assert_eq!(
1020 split_file_hunks(
1021 &[(commit_id1, 0..6) ],
1022 &ContentDiff::by_line(["1a\n1b\n0a\n", "1a\n0a\n"])
1023 ),
1024 hashmap! { commit_id1 => vec![(3..6, 3..3)] }
1025 );
1026 assert_eq!(
1028 split_file_hunks(
1029 &[(commit_id1, 0..6) ],
1030 &ContentDiff::by_line(["1a\n1b\n0a\n", "0a\n"])
1031 ),
1032 hashmap! { commit_id1 => vec![(0..6, 0..0)] }
1033 );
1034 }
1035
1036 #[test]
1037 fn test_split_file_hunks_non_contiguous_tail_range_delete_modify_masked() {
1038 let commit_id1 = &CommitId::from_hex("111111");
1039
1040 assert_eq!(
1042 split_file_hunks(
1043 &[(commit_id1, 0..6) ],
1044 &ContentDiff::by_line(["1a\n1b\n0a\n", "1a\n0A\n"])
1045 ),
1046 hashmap! {}
1047 );
1048 assert_eq!(
1050 split_file_hunks(
1051 &[(commit_id1, 0..6) ],
1052 &ContentDiff::by_line(["1a\n1b\n0a\n", "0A\n"])
1053 ),
1054 hashmap! {}
1055 );
1056 }
1057
1058 #[test]
1059 fn test_split_file_hunks_non_contiguous_tail_range_delete_delete_masked() {
1060 let commit_id1 = &CommitId::from_hex("111111");
1061
1062 assert_eq!(
1067 split_file_hunks(
1068 &[(commit_id1, 0..6) ],
1069 &ContentDiff::by_line(["1a\n1b\n0a\n", "1a\n"])
1070 ),
1071 hashmap! {}
1072 );
1073 assert_eq!(
1075 split_file_hunks(
1076 &[(commit_id1, 0..6) ],
1077 &ContentDiff::by_line(["1a\n1b\n0a\n", ""])
1078 ),
1079 hashmap! {}
1080 );
1081 }
1082
1083 #[test]
1084 fn test_split_file_hunks_non_contiguous_tail_range_modify() {
1085 let commit_id1 = &CommitId::from_hex("111111");
1086
1087 assert_eq!(
1089 split_file_hunks(
1090 &[(commit_id1, 0..6) ],
1091 &ContentDiff::by_line(["1a\n1b\n0a\n", "1a\n1B\n0a\n"])
1092 ),
1093 hashmap! { commit_id1 => vec![(3..6, 3..6)] }
1094 );
1095 }
1096
1097 #[test]
1098 fn test_split_file_hunks_non_contiguous_tail_range_modify_modify_masked() {
1099 let commit_id1 = &CommitId::from_hex("111111");
1100
1101 assert_eq!(
1103 split_file_hunks(
1104 &[(commit_id1, 0..6) ],
1105 &ContentDiff::by_line(["1a\n1b\n0a\n", "1a\n1B\n0A\n"])
1106 ),
1107 hashmap! {}
1108 );
1109 }
1110
1111 #[test]
1112 fn test_split_file_hunks_multiple_edits() {
1113 let commit_id1 = &CommitId::from_hex("111111");
1114 let commit_id2 = &CommitId::from_hex("222222");
1115 let commit_id3 = &CommitId::from_hex("333333");
1116
1117 assert_eq!(
1118 split_file_hunks(
1119 &[
1120 (commit_id1, 0..3), (commit_id2, 3..6), (commit_id1, 6..15), (commit_id3, 15..21), ],
1125 &ContentDiff::by_line([
1126 "1a\n2a\n1b\n1c\n1d\n3a\n3b\n",
1127 "1A\n2a\n1B\n1d\n3X\n3A\n3b\n3Y\n"
1128 ])
1129 ),
1130 hashmap! {
1131 commit_id1 => vec![(0..3, 0..3), (6..12, 6..9)],
1132 commit_id3 => vec![(15..18, 12..18), (21..21, 21..24)],
1133 }
1134 );
1135 }
1136
1137 #[test]
1138 fn test_combine_texts() {
1139 assert_eq!(combine_texts(b"", b"", &[]), "");
1140 assert_eq!(combine_texts(b"foo", b"bar", &[]), "foo");
1141 assert_eq!(combine_texts(b"foo", b"bar", &[(0..3, 0..3)]), "bar");
1142
1143 assert_eq!(
1144 combine_texts(
1145 b"1a\n2a\n1b\n1c\n1d\n3a\n3b\n",
1146 b"1A\n2a\n1B\n1d\n3X\n3A\n3b\n3Y\n",
1147 &[(0..3, 0..3), (6..12, 6..9)]
1148 ),
1149 "1A\n2a\n1B\n1d\n3a\n3b\n"
1150 );
1151 assert_eq!(
1152 combine_texts(
1153 b"1a\n2a\n1b\n1c\n1d\n3a\n3b\n",
1154 b"1A\n2a\n1B\n1d\n3X\n3A\n3b\n3Y\n",
1155 &[(15..18, 12..18), (21..21, 21..24)]
1156 ),
1157 "1a\n2a\n1b\n1c\n1d\n3X\n3A\n3b\n3Y\n"
1158 );
1159 }
1160}