use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::Hash;
use std::hash::Hasher;
use std::path::PathBuf;
use crate::IgnoreWhitespace;
use crate::diff::DiffItem;
use crate::diff::diff;
use crate::files::FilePairEvent;
use crate::tokens::NormalizedBytes;
use crate::tokens::lines_to_bytes;
use crate::tokens::split_lines;
use crate::tokens::tokenize_lines;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DiffSignature {
lhs: u64,
rhs: u64,
}
impl DiffSignature {
pub fn new(
lhs_lines: &[&[u8]],
rhs_lines: &[&[u8]],
mode: IgnoreWhitespace,
) -> DiffSignature {
let lhs_tokens = tokenize_lines(lhs_lines);
let rhs_tokens = tokenize_lines(rhs_lines);
let token_diffs = diff(&lhs_tokens, &rhs_tokens, mode);
let mut lhs_hasher = DefaultHasher::new();
let mut rhs_hasher = DefaultHasher::new();
for item in &token_diffs {
if let DiffItem::Mutation { lhs: tl, rhs: tr } = item {
for &tok in &lhs_tokens[*tl] {
NormalizedBytes::new(tok, mode).hash(&mut lhs_hasher);
}
for &tok in &rhs_tokens[*tr] {
NormalizedBytes::new(tok, mode).hash(&mut rhs_hasher);
}
}
}
DiffSignature {
lhs: lhs_hasher.finish(),
rhs: rhs_hasher.finish(),
}
}
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct ClusterEntry {
pub lhs_path: Option<PathBuf>,
pub rhs_path: Option<PathBuf>,
}
#[derive(Debug)]
pub struct DiffCluster {
pub signature: DiffSignature,
pub entries: BTreeMap<ClusterEntry, usize>,
pub exemplar_lhs: Vec<u8>,
pub exemplar_rhs: Vec<u8>,
}
impl DiffCluster {
pub fn cluster(
events: impl Iterator<Item = FilePairEvent>,
mode: IgnoreWhitespace,
) -> Vec<DiffCluster> {
let mut map: HashMap<DiffSignature, DiffCluster> = HashMap::new();
for event in events {
if let FilePairEvent::TextDiff {
lhs_path,
rhs_path,
lhs_content,
rhs_content,
} = event
{
let lhs_lines = split_lines(&lhs_content);
let rhs_lines = split_lines(&rhs_content);
let line_diffs = diff(&lhs_lines, &rhs_lines, mode);
for item in &line_diffs {
if let DiffItem::Mutation { lhs, rhs } = item {
let lhs = &lhs_lines[*lhs];
let rhs = &rhs_lines[*rhs];
let sig = DiffSignature::new(lhs, rhs, mode);
let cluster =
map.entry(sig.clone()).or_insert_with(|| DiffCluster {
signature: sig,
entries: BTreeMap::new(),
exemplar_lhs: lines_to_bytes(lhs),
exemplar_rhs: lines_to_bytes(rhs),
});
*cluster
.entries
.entry(ClusterEntry {
lhs_path: lhs_path.clone(),
rhs_path: rhs_path.clone(),
})
.or_insert(0) += 1;
}
}
}
}
let mut clusters: Vec<DiffCluster> = map.into_values().collect();
clusters.sort_by(|a, b| {
let a_total: usize = a.entries.values().sum();
let b_total: usize = b.entries.values().sum();
b_total.cmp(&a_total)
});
clusters
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sig(lhs: &[u8], rhs: &[u8], mode: IgnoreWhitespace) -> DiffSignature {
let lhs_lines = split_lines(lhs);
let rhs_lines = split_lines(rhs);
DiffSignature::new(&lhs_lines, &rhs_lines, mode)
}
fn sizes(clusters: &[DiffCluster]) -> Vec<usize> {
clusters.iter().map(|c| c.entries.values().sum()).collect()
}
#[test]
fn signature_whitespace_normalization() {
assert_eq!(
sig(b"x = 1\n", b"x = 1\n", IgnoreWhitespace::LengthChanges),
sig(b"a = b\n", b"a = b\n", IgnoreWhitespace::LengthChanges)
);
}
#[test]
fn signature_different_changes() {
assert_ne!(
sig(b"foo\n", b"bar\n", IgnoreWhitespace::No),
sig(b"foo\n", b"baz\n", IgnoreWhitespace::No)
);
}
#[test]
fn signature_identical_content() {
assert_eq!(
sig(b"hello\n", b"hello\n", IgnoreWhitespace::No),
sig(b"world\n", b"world\n", IgnoreWhitespace::No)
);
}
#[test]
fn group_clusters_basic() {
let events = vec![
FilePairEvent::TextDiff {
lhs_path: Some("a/old.txt".into()),
rhs_path: Some("a/new.txt".into()),
lhs_content: b"foo\n".to_vec(),
rhs_content: b"bar\n".to_vec(),
},
FilePairEvent::TextDiff {
lhs_path: Some("b/old.txt".into()),
rhs_path: Some("b/new.txt".into()),
lhs_content: b"foo\n".to_vec(),
rhs_content: b"bar\n".to_vec(),
},
FilePairEvent::TextDiff {
lhs_path: Some("c/old.txt".into()),
rhs_path: Some("c/new.txt".into()),
lhs_content: b"hello\n".to_vec(),
rhs_content: b"world\n".to_vec(),
},
];
let clusters =
DiffCluster::cluster(events.into_iter(), IgnoreWhitespace::No);
assert_eq!(sizes(&clusters), [2, 1]);
}
#[test]
fn group_clusters_per_mutation() {
let events = vec![FilePairEvent::TextDiff {
lhs_path: Some("f.txt".into()),
rhs_path: Some("f.txt".into()),
lhs_content: b"foo\nkeep\nfoo\n".to_vec(),
rhs_content: b"bar\nkeep\nbar\n".to_vec(),
}];
let clusters =
DiffCluster::cluster(events.into_iter(), IgnoreWhitespace::No);
assert_eq!(sizes(&clusters), [2]);
}
}