use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use super::{
Attribution, BlameError, BlameOptions, BlameOutcome, CopyDetection, find_blob_in_tree,
ignore_fallthrough, is_trivial_key, line_key, load_blob_lines, match_lines_with_options,
move_copy,
};
use crate::hash::Hash;
use crate::object::{Identity, Object};
use crate::store::ObjectStore;
pub(super) struct DagNode {
pub(super) blob_hash: Hash,
author: Identity,
timestamp: u64,
pub(super) parents: Vec<Hash>,
}
impl DagNode {
fn own_attribution(&self, commit_hash: Hash, boundary: bool) -> Attribution {
Attribution {
commit_hash,
author: self.author.clone(),
timestamp: self.timestamp,
orig_line_num: 0,
boundary,
source_path: None,
}
}
}
fn file_blob_at(
store: &ObjectStore,
blob_of: &mut HashMap<Hash, Option<Hash>>,
commit: Hash,
file_path: &str,
) -> BlameOutcome<Option<Hash>> {
if let Some(&blob) = blob_of.get(&commit) {
return Ok(blob);
}
let Object::Commit(c) = store.read_object(&commit)? else {
return Err(BlameError::NotACommit);
};
let blob = find_blob_in_tree(store, c.tree_hash, file_path)?;
blob_of.insert(commit, blob);
Ok(blob)
}
pub(super) fn build_file_dag(
store: &ObjectStore,
head_hash: Hash,
file_path: &str,
first_parent: bool,
) -> BlameOutcome<(HashMap<Hash, DagNode>, HashMap<Hash, usize>)> {
let mut nodes: HashMap<Hash, DagNode> = HashMap::new();
let mut children: HashMap<Hash, usize> = HashMap::new();
let mut blob_of: HashMap<Hash, Option<Hash>> = HashMap::new();
let mut stack = vec![head_hash];
while let Some(commit_hash) = stack.pop() {
if nodes.contains_key(&commit_hash) {
continue;
}
let Object::Commit(commit) = store.read_object(&commit_hash)? else {
return Err(BlameError::NotACommit);
};
let Some(blob_hash) = find_blob_in_tree(store, commit.tree_hash, file_path)? else {
continue;
};
blob_of.entry(commit_hash).or_insert(Some(blob_hash));
let raw_parents: &[Hash] = if first_parent {
commit.parents.get(..1).unwrap_or(&[])
} else {
&commit.parents
};
let mut relevant = Vec::new();
for &parent in raw_parents {
if file_blob_at(store, &mut blob_of, parent, file_path)?.is_some() {
relevant.push(parent);
*children.entry(parent).or_insert(0) += 1;
if !nodes.contains_key(&parent) {
stack.push(parent);
}
}
}
nodes.insert(
commit_hash,
DagNode {
blob_hash,
author: commit.author.clone(),
timestamp: commit.timestamp,
parents: relevant,
},
);
}
Ok((nodes, children))
}
pub(super) fn topo_order(nodes: &HashMap<Hash, DagNode>, head: Hash) -> Vec<Hash> {
let mut order = Vec::with_capacity(nodes.len());
let mut visited: HashSet<Hash> = HashSet::new();
let mut stack: Vec<(Hash, bool)> = vec![(head, false)];
while let Some((commit, emit)) = stack.pop() {
if emit {
order.push(commit);
continue;
}
if !visited.insert(commit) {
continue;
}
stack.push((commit, true));
for &parent in &nodes[&commit].parents {
if !visited.contains(&parent) {
stack.push((parent, false));
}
}
}
order
}
type ParentData<'a> = (Cow<'a, [Vec<u8>]>, Vec<Option<usize>>);
pub(super) struct WalkCtx<'a> {
pub(super) store: &'a ObjectStore,
pub(super) opts: &'a BlameOptions,
pub(super) nodes: &'a HashMap<Hash, DagNode>,
pub(super) file_path: &'a str,
}
struct CommitPass<'a> {
node: &'a DagNode,
commit: Hash,
lines: &'a [Vec<u8>],
first_parent_lines: &'a [Vec<u8>],
}
impl<'a> CommitPass<'a> {
fn parent_lines(
&self,
ctx: &WalkCtx,
k: usize,
parent: Hash,
) -> BlameOutcome<Cow<'a, [Vec<u8>]>> {
Ok(if k == 0 {
Cow::Borrowed(self.first_parent_lines)
} else {
Cow::Owned(load_blob_lines(ctx.store, ctx.nodes[&parent].blob_hash)?)
})
}
}
pub(super) fn attribute_commit(
ctx: &WalkCtx,
memo: &HashMap<Hash, Rc<[Attribution]>>,
detector: &mut move_copy::Detector,
commit: Hash,
) -> BlameOutcome<Rc<[Attribution]>> {
let node = &ctx.nodes[&commit];
if node.parents.len() == 1 && ctx.nodes[&node.parents[0]].blob_hash == node.blob_hash {
return Ok(Rc::clone(&memo[&node.parents[0]]));
}
let lines = load_blob_lines(ctx.store, node.blob_hash)?;
let boundary = node.parents.is_empty();
let own_attrs = |len: usize| -> Vec<Attribution> {
(0..len)
.map(|i| {
let mut a = node.own_attribution(commit, boundary);
a.orig_line_num = i + 1;
a
})
.collect()
};
if node.parents.is_empty() {
let mut attrs = own_attrs(lines.len());
if matches!(ctx.opts.copies, CopyDetection::On { .. }) {
let pass = CommitPass {
node,
commit,
lines: &lines,
first_parent_lines: &[],
};
let mut claimed = vec![false; lines.len()];
apply_detection(ctx, memo, detector, &pass, &[], &mut attrs, &mut claimed)?;
}
return Ok(attrs.into());
}
let first_parent = node.parents[0];
let mut attrs = own_attrs(lines.len());
let mut matched = vec![false; lines.len()];
let first_parent_lines = load_blob_lines(ctx.store, ctx.nodes[&first_parent].blob_hash)?;
let pass = CommitPass {
node,
commit,
lines: &lines,
first_parent_lines: &first_parent_lines,
};
let mut parents_data: Vec<ParentData> = Vec::with_capacity(node.parents.len());
for (k, &parent) in node.parents.iter().enumerate() {
let parent_lines = pass.parent_lines(ctx, k, parent)?;
let mapping = match_lines_with_options(&parent_lines, &lines, ctx.opts)?;
let parent_attrs = &memo[&parent];
for (ni, m) in mapping.iter().enumerate() {
if matched[ni] {
continue; }
let Some(oi) = *m else { continue };
if oi < parent_attrs.len() {
attrs[ni] = parent_attrs[oi].clone();
matched[ni] = true;
}
}
parents_data.push((parent_lines, mapping));
}
if ctx.opts.is_ignored(&commit) {
apply_ignore_fallthrough(ctx, memo, &pass, &parents_data, &mut attrs, &mut matched);
}
if ctx.opts.detection_enabled() {
apply_detection(
ctx,
memo,
detector,
&pass,
&parents_data,
&mut attrs,
&mut matched,
)?;
}
Ok(attrs.into())
}
fn apply_ignore_fallthrough(
ctx: &WalkCtx,
memo: &HashMap<Hash, Rc<[Attribution]>>,
pass: &CommitPass,
parents_data: &[ParentData],
attrs: &mut [Attribution],
matched: &mut [bool],
) {
for (k, &parent) in pass.node.parents.iter().enumerate() {
let (parent_lines, mapping) = &parents_data[k];
let fall = ignore_fallthrough(mapping, parent_lines.len());
let fall = if ctx.opts.ignore_rev_precise {
precise_overrides(&PreciseRequest {
mapping,
fall: &fall,
new_lines: pass.lines,
parent_lines,
matched,
ignore_whitespace: ctx.opts.ignore_whitespace,
})
} else {
fall
};
let parent_attrs = &memo[&parent];
for (ni, &paired) in fall.iter().enumerate() {
if matched[ni] {
continue;
}
let Some(oi) = paired else { continue };
if oi < parent_attrs.len() {
attrs[ni] = parent_attrs[oi].clone();
matched[ni] = true;
}
}
}
}
fn apply_detection(
ctx: &WalkCtx,
memo: &HashMap<Hash, Rc<[Attribution]>>,
detector: &mut move_copy::Detector,
pass: &CommitPass,
parents_data: &[ParentData],
attrs: &mut [Attribution],
matched: &mut [bool],
) -> BlameOutcome<()> {
let mut real_parents = move_copy::commit_parents(ctx.store, pass.commit)?;
if ctx.opts.first_parent {
real_parents.truncate(1);
}
let copies_on = matches!(ctx.opts.copies, CopyDetection::On { .. });
let mut seen_blobs: HashSet<Hash> = HashSet::new();
for &parent in &real_parents {
if matched.iter().all(|&m| m) {
break; }
let porigin = match pass.node.parents.iter().position(|&p| p == parent) {
Some(k) if seen_blobs.insert(ctx.nodes[&parent].blob_hash) => Some(k),
_ => None,
};
if porigin.is_none() && !copies_on {
continue;
}
detector.reassign(
&move_copy::ReassignRequest {
file_path: ctx.file_path,
source_commit: parent,
attributed_commit: pass.commit,
new_lines: pass.lines,
within_file: porigin.map(|k| {
(
&*parents_data[k].0,
&*memo[&pass.node.parents[k]] as &[Attribution],
)
}),
},
attrs,
matched,
)?;
}
Ok(())
}
const PRECISE_MAX_CANDIDATES_PER_KEY: usize = 256;
const PRECISE_MAX_RUN_WORK: usize = 1 << 22;
pub(super) struct PreciseRequest<'r> {
pub(super) mapping: &'r [Option<usize>],
pub(super) fall: &'r [Option<usize>],
pub(super) new_lines: &'r [Vec<u8>],
pub(super) parent_lines: &'r [Vec<u8>],
pub(super) matched: &'r [bool],
pub(super) ignore_whitespace: bool,
}
struct Proposal {
ni: usize,
real_old: usize,
run_len: usize,
}
pub(super) fn precise_overrides(req: &PreciseRequest) -> Vec<Option<usize>> {
let &PreciseRequest {
mapping,
fall,
new_lines,
parent_lines,
matched,
ignore_whitespace: iw,
} = req;
let unmatched_new: Vec<usize> = (0..mapping.len())
.filter(|&ni| mapping[ni].is_none() && !matched[ni])
.collect();
if unmatched_new.is_empty() {
return fall.to_vec();
}
let matched_old: HashSet<usize> = mapping.iter().filter_map(|&m| m).collect();
let unmatched_old: Vec<usize> = (0..parent_lines.len())
.filter(|oi| !matched_old.contains(oi))
.collect();
if unmatched_old.is_empty() {
return fall.to_vec();
}
let old_keys: Vec<Vec<u8>> = unmatched_old
.iter()
.map(|&oi| line_key(&parent_lines[oi], iw))
.collect();
let old_index = move_copy::build_index(&old_keys);
let new_keys: Vec<Vec<u8>> = unmatched_new
.iter()
.map(|&ni| line_key(&new_lines[ni], iw))
.collect();
let mut keeps: HashSet<usize> = HashSet::new(); let mut claimed: HashSet<usize> = HashSet::new(); for (pos, &ni) in unmatched_new.iter().enumerate() {
let Some(j) = fall[ni] else { continue };
let key = &new_keys[pos];
if is_trivial_key(key) || *line_key(&parent_lines[j], iw) == **key {
keeps.insert(ni);
claimed.insert(j);
}
}
let mut proposals: Vec<Proposal> = Vec::new();
let mut work = 0usize;
for (pos, &ni) in unmatched_new.iter().enumerate() {
let key = &new_keys[pos];
if keeps.contains(&ni) || is_trivial_key(key) {
continue; }
let Some(offsets) = old_index.get(key) else {
continue; };
if offsets.len() > PRECISE_MAX_CANDIDATES_PER_KEY {
continue; }
let Some((d, run_len)) = best_content_candidate(
pos,
offsets,
&unmatched_new,
&new_keys,
&old_keys,
&unmatched_old,
fall[ni],
&mut work,
) else {
continue;
};
if work > PRECISE_MAX_RUN_WORK {
return fall.to_vec(); }
if fall[ni].is_some() && run_len < 2 {
continue;
}
proposals.push(Proposal {
ni,
real_old: unmatched_old[d],
run_len,
});
}
proposals.sort_by(|a, b| b.run_len.cmp(&a.run_len).then(a.ni.cmp(&b.ni)));
let mut out = fall.to_vec();
for p in proposals {
if claimed.insert(p.real_old) {
out[p.ni] = Some(p.real_old);
}
}
out
}
#[allow(clippy::too_many_arguments)]
fn best_content_candidate(
pos: usize,
offsets: &[usize],
unmatched_new: &[usize],
new_keys: &[Vec<u8>],
old_keys: &[Vec<u8>],
unmatched_old: &[usize],
positional: Option<usize>,
work: &mut usize,
) -> Option<(usize, usize)> {
let mut run_len = |d: usize| -> usize {
let mut k = 0usize;
while pos + k < new_keys.len()
&& d + k < old_keys.len()
&& new_keys[pos + k] == old_keys[d + k]
&& (k == 0
|| (unmatched_new[pos + k] == unmatched_new[pos + k - 1] + 1
&& unmatched_old[d + k] == unmatched_old[d + k - 1] + 1))
{
k += 1;
*work += 1;
}
k
};
let mut best: Option<(usize, usize)> = None; for &d in offsets {
let len = run_len(d);
let better = match best {
None => true,
Some((bd, bl)) => match len.cmp(&bl) {
Ordering::Greater => true,
Ordering::Less => false,
Ordering::Equal => match positional {
Some(expected) => {
let cur = unmatched_old[d].abs_diff(expected);
let prev = unmatched_old[bd].abs_diff(expected);
cur < prev || (cur == prev && unmatched_old[d] < unmatched_old[bd])
}
None => unmatched_old[d] < unmatched_old[bd],
},
},
};
if better {
best = Some((d, len));
}
}
best
}