Skip to main content

objects/blame/
finalize.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Build canonical FileProvenance from a completed origin stream.
3
4use super::types::{BlameSliceError, OriginRange};
5use crate::object::{ContentHash, FileProvenance, Origin, OriginSet};
6
7/// Assemble [`FileProvenance`] from finalized ranges without holding history.
8///
9/// Ranges are sorted, checked for exact coverage, and coalesced when adjacent
10/// ranges share the same origin.
11pub fn finalize_file_provenance(
12    file_blob: ContentHash,
13    line_count: u32,
14    ranges: impl IntoIterator<Item = OriginRange>,
15) -> Result<FileProvenance, BlameSliceError> {
16    let mut ranges: Vec<OriginRange> = ranges.into_iter().collect();
17    ranges.sort_by_key(|range| range.target_start);
18
19    if line_count == 0 {
20        if ranges
21            .iter()
22            .any(|range| range.target_start != 0 || range.len != 0)
23        {
24            return Err(BlameSliceError::InvalidCoverage);
25        }
26        let origins = ranges
27            .first()
28            .map(|range| vec![range.origin.clone()])
29            .unwrap_or_default();
30        let origin_sets = if origins.is_empty() {
31            Vec::new()
32        } else {
33            vec![OriginSet {
34                origin_indexes: vec![0],
35            }]
36        };
37        let provenance = FileProvenance::new(file_blob, 0, Vec::new(), origins, origin_sets);
38        provenance
39            .validate()
40            .map_err(|_| BlameSliceError::InvalidCoverage)?;
41        return Ok(provenance);
42    }
43
44    let coalesced = coalesce(ranges);
45    let mut next_line = 0u32;
46    let mut origins: Vec<Origin> = Vec::new();
47    let mut origin_sets = Vec::new();
48    let mut spans = Vec::new();
49
50    for range in coalesced {
51        if range.target_start != next_line || range.len == 0 {
52            return Err(BlameSliceError::InvalidCoverage);
53        }
54        let origin_index = origin_index(&mut origins, range.origin);
55        let origin_set_index = origin_set_index(&mut origin_sets, origin_index);
56        spans.push(crate::object::LineSpan {
57            start_line: range.target_start,
58            line_len: range.len,
59            origin_set_index,
60        });
61        next_line = next_line.saturating_add(range.len);
62    }
63    if next_line != line_count {
64        return Err(BlameSliceError::InvalidCoverage);
65    }
66
67    let provenance = FileProvenance::new(file_blob, line_count, spans, origins, origin_sets);
68    provenance
69        .validate()
70        .map_err(|_| BlameSliceError::InvalidCoverage)?;
71    Ok(provenance)
72}
73
74fn coalesce(ranges: Vec<OriginRange>) -> Vec<OriginRange> {
75    let mut out: Vec<OriginRange> = Vec::new();
76    for range in ranges {
77        match out.last_mut() {
78            Some(last)
79                if last.target_start + last.len == range.target_start
80                    && last.origin == range.origin =>
81            {
82                last.len += range.len;
83            }
84            _ => out.push(range),
85        }
86    }
87    out
88}
89
90fn origin_index(origins: &mut Vec<Origin>, origin: Origin) -> u32 {
91    if let Some((index, _)) = origins
92        .iter()
93        .enumerate()
94        .find(|(_, existing)| **existing == origin)
95    {
96        return index as u32;
97    }
98    let next = origins.len() as u32;
99    origins.push(origin);
100    next
101}
102
103fn origin_set_index(origin_sets: &mut Vec<OriginSet>, origin_index: u32) -> u32 {
104    let indexes = vec![origin_index];
105    if let Some((index, _)) = origin_sets
106        .iter()
107        .enumerate()
108        .find(|(_, set)| set.origin_indexes == indexes)
109    {
110        return index as u32;
111    }
112    let next = origin_sets.len() as u32;
113    origin_sets.push(OriginSet {
114        origin_indexes: indexes,
115    });
116    next
117}