Skip to main content

git_zero_core/validation/
revision.rs

1//! Revision-list history cuts. Boundary membership is independent of PACK membership.
2use super::*;
3
4mod date;
5mod state;
6use date::commit_date;
7use state::{BORDER, CANDIDATE, CLIENT_SHALLOW, DONE, EXCLUDED, PENDING, UNSHALLOW};
8pub use state::{RevisionLimits, RevisionOptions, RevisionState, ShallowChange};
9
10impl<R: Repository + ?Sized> ValidatedRepository<'_, '_, R> {
11    /// Select revision-limited history with separate caller-owned boundary state.
12    /// `history` contains common commits, client boundaries, and resolved exclusion
13    /// endpoints. This additive path rejects an absolute depth, preserves blob
14    /// filtering/ascending indices, and shares structural bytes across every pass.
15    /// Exclusion and candidate passes visit commits only; payload traversal follows
16    /// trees/tags with the existing validated graph contract. No blob body is read.
17    /// Existing shallow commits are known, and unshallow transitions seed parents;
18    /// common haves outside candidate history cannot hide newly requested history.
19    /// Metadata scans are bounded by object capacity (quadratic worst case), and
20    /// exclusion count is capped independently. Total preparation CPU accounting
21    /// is a separate host concern, as with ordinary bounded selection.
22    pub fn select_revision_fetch<'selected>(
23        &self,
24        roots: &[ObjectId],
25        history: (
26            impl IntoIterator<Item = Result<ObjectId>>,
27            impl IntoIterator<Item = Result<ObjectId>>,
28            impl IntoIterator<Item = Result<ObjectId>>,
29        ),
30        work: &mut [Option<ValidationMetadata>],
31        selected: &'selected mut [usize],
32        scratch: (&mut [u8], &mut [u8], &mut RevisionState<'_>),
33        options: (SelectionOptions, FetchOptions, RevisionOptions),
34    ) -> Result<&'selected [usize]> {
35        let (read, parse, revision) = scratch;
36        revision.count = 0;
37        let count = self.objects.len();
38        let (selection, fetch, options) = options;
39        if roots.is_empty() || fetch.depth.is_some() || options.since == Some(u64::MAX) {
40            return Err(Error::InvalidRequest);
41        }
42        if matches!(fetch.filter, BlobFilter::Type(_) | BlobFilter::TreeDepth(_)) {
43            return Err(Error::Unsupported);
44        }
45        if work.len() < count || selected.len() < count || revision.states.len() < count {
46            return Err(Error::BufferTooSmall);
47        }
48
49        let (common, shallow, excluded) = history;
50        let mut payload = Selection {
51            repository: self,
52            work: &mut work[..count],
53            scratch: (read, parse),
54            remaining: selection.graph_read_limit,
55            filter: BlobFilter::All,
56        };
57        payload.prepare(shallow)?;
58        for (state, object) in revision.states[..count].iter_mut().zip(payload.work.iter()) {
59            *state = u8::from(object.expect("validated metadata").client_shallow) * CLIENT_SHALLOW;
60        }
61        let mut walk = Walker {
62            repository: self,
63            objects: payload.work,
64            states: &mut revision.states[..count],
65            read: payload.scratch.0,
66            parse: payload.scratch.1,
67            bytes: payload.remaining,
68        };
69        walk.exclude(excluded, revision.limits.max_exclusions)?;
70        walk.candidates(roots, options.since)?;
71        walk.exclude_common(common)?;
72        walk.wanted_payload(roots)?;
73        filter_objects(walk.objects, fetch.filter);
74        if selection.include_tags {
75            walk.include_tags()?;
76            walk.payload(&[])?;
77            filter_objects(walk.objects, fetch.filter);
78        }
79        let length = selected_indices(walk.objects, selected);
80        revision.count = count;
81        Ok(&selected[..length])
82    }
83}
84
85#[derive(Clone, Copy)]
86enum Parents {
87    Pending,
88    Boundary,
89    Seed,
90}
91
92fn mark_parent(
93    action: Parents,
94    index: usize,
95    parent: usize,
96    objects: &mut [Option<ValidationMetadata>],
97    states: &mut [u8],
98) {
99    match action {
100        Parents::Pending => states[parent] |= PENDING,
101        Parents::Boundary if states[parent] & CANDIDATE == 0 => states[index] |= BORDER,
102        Parents::Seed => {
103            let object = metadata_mut(objects, parent);
104            if !object.excluded {
105                object.reached = true;
106                object.depth = 1;
107            }
108        }
109        _ => {}
110    }
111}
112
113struct Walker<'a, 'b, 'index, 'repo, R: Repository + ?Sized> {
114    repository: &'a ValidatedRepository<'index, 'repo, R>,
115    objects: &'b mut [Option<ValidationMetadata>],
116    states: &'b mut [u8],
117    read: &'b mut [u8],
118    parse: &'b mut [u8],
119    bytes: u64,
120}
121
122impl<R: Repository + ?Sized> Walker<'_, '_, '_, '_, R> {
123    fn exclude(
124        &mut self,
125        excluded: impl IntoIterator<Item = Result<ObjectId>>,
126        limit: usize,
127    ) -> Result<()> {
128        for (position, id) in excluded.into_iter().enumerate() {
129            if position >= limit {
130                return Err(Error::LimitExceeded);
131            }
132            let index = self.find(id?)?;
133            self.states[index] |= PENDING;
134        }
135        self.ancestry(None, EXCLUDED)
136    }
137
138    fn candidates(&mut self, roots: &[ObjectId], since: Option<u64>) -> Result<()> {
139        for &id in roots {
140            let index = self.find(id)?;
141            self.queue_commit(index)?;
142        }
143        self.ancestry(since.filter(|&value| value != 0), CANDIDATE)?;
144
145        if !self.states.iter().any(|state| state & CANDIDATE != 0) {
146            return Err(Error::InvalidRequest);
147        }
148        // Freeze candidate membership before marking borders: a merge border
149        // may stop another candidate from appearing in the eventual payload.
150        self.visit_parents(CANDIDATE, Parents::Boundary)?;
151        state::mark_unshallow(self.states);
152        Ok(())
153    }
154
155    fn exclude_common(&mut self, common: impl IntoIterator<Item = Result<ObjectId>>) -> Result<()> {
156        for id in common {
157            let index = self.find(id?)?;
158            if self.states[index] & CANDIDATE != 0 {
159                metadata_mut(self.objects, index).reached = true;
160            }
161        }
162        self.payload(&[])?;
163        for index in 0..self.objects.len() {
164            let object = metadata_mut(self.objects, index);
165            object.excluded = object.reached || self.states[index] & UNSHALLOW != 0;
166            object.reset_walk();
167        }
168        Ok(())
169    }
170
171    fn wanted_payload(&mut self, roots: &[ObjectId]) -> Result<()> {
172        self.visit_parents(UNSHALLOW, Parents::Seed)?;
173        self.payload(roots)
174    }
175
176    fn include_tags(&mut self) -> Result<()> {
177        Selection {
178            repository: self.repository,
179            work: self.objects,
180            scratch: (self.read, self.parse),
181            remaining: self.bytes,
182            filter: BlobFilter::All,
183        }
184        .include_matching_tags()
185    }
186
187    fn find(&self, id: ObjectId) -> Result<usize> {
188        find(self.objects, self.repository.by_id, id).ok_or(Error::ObjectNotFound)
189    }
190    fn peel(&mut self, mut index: usize) -> Result<usize> {
191        for _ in 0..self.objects.len() {
192            if metadata(self.objects, index).info.kind != ObjectKind::Tag {
193                return Ok(index);
194            }
195            let mut body =
196                Body::budgeted(self.repository, index, self.read, Some(&mut self.bytes))?;
197            let line = body.line(self.parse)?;
198            let id = link(line, b"object ", self.repository.object_format())?;
199            index = self.find(id)?;
200        }
201        Err(Error::InvalidObject)
202    }
203    // Exclusions and explicit wants both peel annotated endpoints before queuing.
204    fn queue_commit(&mut self, index: usize) -> Result<()> {
205        let index = self.peel(index)?;
206        if metadata(self.objects, index).info.kind == ObjectKind::Commit {
207            self.states[index] |= PENDING;
208        }
209        Ok(())
210    }
211    fn ancestry(&mut self, since: Option<u64>, flag: u8) -> Result<()> {
212        while let Some(index) = self
213            .states
214            .iter()
215            .position(|state| state & (PENDING | DONE) == PENDING)
216        {
217            self.states[index] |= DONE;
218            self.visit_ancestor(index, since, flag)?;
219        }
220
221        for state in self.states.iter_mut() {
222            *state &= !(PENDING | DONE);
223        }
224        Ok(())
225    }
226    fn visit_ancestor(&mut self, index: usize, since: Option<u64>, flag: u8) -> Result<()> {
227        if metadata(self.objects, index).info.kind != ObjectKind::Commit {
228            return self.queue_commit(index);
229        }
230        if flag == CANDIDATE && self.states[index] & EXCLUDED != 0 {
231            return Ok(());
232        }
233        if let Some(cut) = since {
234            if self.date(index)? < cut {
235                return Ok(());
236            }
237        }
238        self.states[index] |= flag;
239        self.commit(index, Parents::Pending)
240    }
241    fn date(&mut self, index: usize) -> Result<u64> {
242        let mut body = Body::budgeted(self.repository, index, self.read, Some(&mut self.bytes))?;
243        commit_tree(&mut body, self.parse)?;
244        let committer = commit_parents(&mut body, self.parse, |_| Ok(()))?;
245        Ok(commit_date(committer))
246    }
247
248    // Both parent passes use stable provider order with their own state mask
249    // and parent transition.
250    fn visit_parents(&mut self, flag: u8, action: Parents) -> Result<()> {
251        for index in 0..self.states.len() {
252            if self.states[index] & flag != 0 {
253                self.commit(index, action)?;
254            }
255        }
256        Ok(())
257    }
258
259    fn commit(&mut self, index: usize, action: Parents) -> Result<()> {
260        let mut body = Body::budgeted(self.repository, index, self.read, Some(&mut self.bytes))?;
261        commit_tree(&mut body, self.parse)?;
262        commit_parents(&mut body, self.parse, |id| {
263            let parent =
264                find(self.objects, self.repository.by_id, id).ok_or(Error::ObjectNotFound)?;
265            mark_parent(action, index, parent, self.objects, self.states);
266            Ok(())
267        })?;
268        Ok(())
269    }
270    fn payload(&mut self, roots: &[ObjectId]) -> Result<()> {
271        // Revision borders temporarily act as client stops for the shared
272        // wanted-object walker. Caller markers are restored on success.
273        self.payload_boundaries(false);
274        let mut payload = Selection {
275            repository: self.repository,
276            work: self.objects,
277            scratch: (self.read, self.parse),
278            remaining: self.bytes,
279            filter: BlobFilter::All,
280        };
281        let result = payload.walk_wants(
282            roots,
283            History {
284                client: true,
285                ..History::default()
286            },
287        );
288        self.bytes = payload.remaining;
289        result?;
290        self.payload_boundaries(true);
291        Ok(())
292    }
293
294    fn payload_boundaries(&mut self, restore: bool) {
295        for (object, &state) in self.objects.iter_mut().zip(self.states.iter()) {
296            object.as_mut().expect("validated metadata").client_shallow =
297                state & CLIENT_SHALLOW != 0 || (!restore && state & BORDER != 0);
298        }
299    }
300}