Skip to main content

objects/blame/
run.rs

1// SPDX-License-Identifier: Apache-2.0
2//! One-shot blame that loops storage-neutral slices until complete.
3
4use std::{cell::RefCell, collections::HashMap, path::Path};
5
6use crate::object::{Blob, ContentHash, FileProvenance, ObjectSource, State, StateId, Tree};
7
8use super::{
9    advance::advance_file_blame_slice,
10    finalize::finalize_file_provenance,
11    prepare::prepare_file_blame,
12    types::{
13        BlamePreparation, BlameSliceAdvance, BlameSliceError, BlameSliceLimits, BlameTarget,
14        OriginRange,
15    },
16};
17
18/// Resolved objects shared by every slice of one local blame walk.
19///
20/// Preparing a blame and advancing its frontier revisit the target objects;
21/// parents are likewise loaded once while being claimed and again if they
22/// become the next frontier. Keeping this cache at the one-shot boundary
23/// avoids re-decoding those objects without making resumable slices stateful.
24struct BlameObjectCache<'source, S> {
25    source: &'source S,
26    trees: RefCell<HashMap<ContentHash, Option<Tree>>>,
27    states: RefCell<HashMap<StateId, Option<State>>>,
28    blobs: RefCell<HashMap<ContentHash, Option<Blob>>>,
29}
30
31impl<'source, S> BlameObjectCache<'source, S> {
32    fn new(source: &'source S) -> Self {
33        Self {
34            source,
35            trees: RefCell::new(HashMap::new()),
36            states: RefCell::new(HashMap::new()),
37            blobs: RefCell::new(HashMap::new()),
38        }
39    }
40}
41
42impl<S: ObjectSource> ObjectSource for BlameObjectCache<'_, S> {
43    fn get_tree(&self, hash: &ContentHash) -> crate::error::Result<Option<Tree>> {
44        if let Some(tree) = self.trees.borrow().get(hash).cloned() {
45            return Ok(tree);
46        }
47        let tree = self.source.get_tree(hash)?;
48        self.trees.borrow_mut().insert(*hash, tree.clone());
49        Ok(tree)
50    }
51
52    fn get_state(&self, id: &StateId) -> crate::error::Result<Option<State>> {
53        if let Some(state) = self.states.borrow().get(id).cloned() {
54            return Ok(state);
55        }
56        let state = self.source.get_state(id)?;
57        self.states.borrow_mut().insert(*id, state.clone());
58        Ok(state)
59    }
60
61    fn get_blob(&self, hash: &ContentHash) -> crate::error::Result<Option<Blob>> {
62        if let Some(blob) = self.blobs.borrow().get(hash).cloned() {
63            return Ok(blob);
64        }
65        let blob = self.source.get_blob(hash)?;
66        self.blobs.borrow_mut().insert(*hash, blob.clone());
67        Ok(blob)
68    }
69
70    fn decoded_blob_len(&self, hash: &ContentHash) -> crate::error::Result<Option<u64>> {
71        if let Some(blob) = self.blobs.borrow().get(hash) {
72            return Ok(blob.as_ref().map(|blob| blob.content().len() as u64));
73        }
74        self.source.decoded_blob_len(hash)
75    }
76}
77
78/// Walk `path` at `state` by repeating [`advance_file_blame_slice`] until the
79/// frontier is exhausted, then finalize. This is not an eager full-file shim;
80/// each slice stays inside `limits`.
81pub fn blame_file<S: ObjectSource>(
82    source: &S,
83    state: &State,
84    path: &Path,
85    limits: BlameSliceLimits,
86) -> Result<FileProvenance, BlameSliceError> {
87    let source = BlameObjectCache::new(source);
88    match prepare_file_blame(&source, state, path, limits)? {
89        BlamePreparation::MissingPath => Err(BlameSliceError::MissingPath),
90        BlamePreparation::Unblamable => Err(BlameSliceError::Unblamable),
91        BlamePreparation::Empty { file_blob, origin } => finalize_file_provenance(
92            file_blob,
93            0,
94            [OriginRange {
95                target_start: 0,
96                len: 0,
97                origin,
98            }],
99        ),
100        BlamePreparation::Active {
101            file_blob,
102            line_count,
103            mut frontier,
104        } => {
105            let expected = BlameTarget::bind(state.id(), path, file_blob, line_count)?;
106            frontier.require_target(&expected)?;
107            let mut finalized = Vec::new();
108            loop {
109                frontier.require_target(&expected)?;
110                match advance_file_blame_slice(&source, path, frontier, limits)? {
111                    BlameSliceAdvance::Progress {
112                        next,
113                        finalized: more,
114                        ..
115                    } => {
116                        finalized.extend(more);
117                        frontier = next;
118                    }
119                    BlameSliceAdvance::Complete {
120                        finalized: more, ..
121                    } => {
122                        finalized.extend(more);
123                        break;
124                    }
125                }
126            }
127            finalize_file_provenance(file_blob, line_count, finalized)
128        }
129    }
130}