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