Skip to main content

gix_traverse/commit/topo/
iter.rs

1use gix_hash::{ObjectId, oid};
2use gix_revwalk::PriorityQueue;
3use smallvec::SmallVec;
4
5use crate::commit::{
6    Either, Info, Parents, Topo, find,
7    topo::{Error, Sorting, WalkFlags},
8};
9
10pub(in crate::commit) type GenAndCommitTime = (u32, i64);
11
12// Git's priority queue works as a LIFO stack if no compare function is set,
13// which is the case for `--topo-order.` However, even in that case the initial
14// items of the queue are sorted according to the commit time before beginning
15// the walk.
16#[derive(Debug)]
17pub(in crate::commit) enum Queue {
18    Date(PriorityQueue<i64, Info>),
19    Topo(Vec<(i64, Info)>),
20}
21
22impl Queue {
23    pub(super) fn new(s: Sorting) -> Self {
24        match s {
25            Sorting::DateOrder => Self::Date(PriorityQueue::new()),
26            Sorting::TopoOrder => Self::Topo(vec![]),
27        }
28    }
29
30    pub(super) fn push(&mut self, commit_time: i64, info: Info) {
31        match self {
32            Self::Date(q) => q.insert(commit_time, info),
33            Self::Topo(q) => q.push((commit_time, info)),
34        }
35    }
36
37    fn pop(&mut self) -> Option<Info> {
38        match self {
39            Self::Date(q) => q.pop().map(|(_, info)| info),
40            Self::Topo(q) => q.pop().map(|(_, info)| info),
41        }
42    }
43
44    pub(super) fn initial_sort(&mut self) {
45        if let Self::Topo(inner_vec) = self {
46            inner_vec.sort_by_key(|a| a.0);
47        }
48    }
49}
50
51impl<Find, Predicate> Topo<Find, Predicate>
52where
53    Find: gix_object::Find,
54{
55    pub(super) fn compute_indegrees_to_depth(&mut self, gen_cutoff: u32) -> Result<(), Error> {
56        while let Some(((generation, _), _)) = self.indegree_queue.peek() {
57            if *generation >= gen_cutoff {
58                self.indegree_walk_step()?;
59            } else {
60                break;
61            }
62        }
63
64        Ok(())
65    }
66
67    fn indegree_walk_step(&mut self) -> Result<(), Error> {
68        if let Some(((generation, _), id)) = self.indegree_queue.pop() {
69            self.explore_to_depth(generation)?;
70
71            let parents = self.collect_parents(&id)?;
72            for (id, gen_time) in parents {
73                self.indegrees.entry(id).and_modify(|e| *e += 1).or_insert(2);
74
75                let state = self.states.get_mut(&id).ok_or(Error::MissingStateUnexpected)?;
76                if !state.contains(WalkFlags::InDegree) {
77                    *state |= WalkFlags::InDegree;
78                    self.indegree_queue.insert(gen_time, id);
79                }
80            }
81        }
82        Ok(())
83    }
84
85    fn explore_to_depth(&mut self, gen_cutoff: u32) -> Result<(), Error> {
86        while let Some(((generation, _), _)) = self.explore_queue.peek() {
87            if *generation >= gen_cutoff {
88                self.explore_walk_step()?;
89            } else {
90                break;
91            }
92        }
93        Ok(())
94    }
95
96    fn explore_walk_step(&mut self) -> Result<(), Error> {
97        if let Some((_, id)) = self.explore_queue.pop() {
98            let parents = self.collect_parents(&id)?;
99            self.process_parents(&id, &parents)?;
100
101            for (id, gen_time) in parents {
102                let state = self.states.get_mut(&id).ok_or(Error::MissingStateUnexpected)?;
103
104                if !state.contains(WalkFlags::Explored) {
105                    *state |= WalkFlags::Explored;
106                    self.explore_queue.insert(gen_time, id);
107                }
108            }
109        }
110        Ok(())
111    }
112
113    fn expand_topo_walk(&mut self, id: &oid) -> Result<(), Error> {
114        let parents = self.collect_parents(id)?;
115        self.process_parents(id, &parents)?;
116
117        for (pid, (parent_gen, parent_commit_time)) in parents {
118            let parent_state = self.states.get(&pid).ok_or(Error::MissingStateUnexpected)?;
119            if parent_state.contains(WalkFlags::Uninteresting) {
120                continue;
121            }
122
123            if parent_gen < self.min_gen {
124                self.min_gen = parent_gen;
125                self.compute_indegrees_to_depth(self.min_gen)?;
126            }
127
128            let i = self.indegrees.get_mut(&pid).ok_or(Error::MissingIndegreeUnexpected)?;
129            *i -= 1;
130            if *i != 1 {
131                continue;
132            }
133
134            let parent_ids = self.collect_all_parents(&pid)?.into_iter().map(|e| e.0).collect();
135            self.topo_queue.push(
136                parent_commit_time,
137                Info {
138                    id: pid,
139                    parent_ids,
140                    commit_time: Some(parent_commit_time),
141                },
142            );
143        }
144
145        Ok(())
146    }
147
148    fn process_parents(&mut self, id: &oid, parents: &[(ObjectId, GenAndCommitTime)]) -> Result<(), Error> {
149        let state = self.states.get_mut(id).ok_or(Error::MissingStateUnexpected)?;
150        if state.contains(WalkFlags::Added) {
151            return Ok(());
152        }
153
154        *state |= WalkFlags::Added;
155
156        // If the current commit is uninteresting we pass that on to ALL
157        // parents, otherwise we set the Seen flag.
158        let (pass, insert) = if state.contains(WalkFlags::Uninteresting) {
159            let flags = WalkFlags::Uninteresting;
160            for (id, _) in parents {
161                let grand_parents = self.collect_all_parents(id)?;
162
163                for (id, _) in &grand_parents {
164                    self.states
165                        .entry(*id)
166                        .and_modify(|s| *s |= WalkFlags::Uninteresting)
167                        .or_insert(WalkFlags::Uninteresting | WalkFlags::Seen);
168                }
169            }
170            (flags, flags)
171        } else {
172            // NOTE: git sets SEEN like we do but keeps the SYMMETRIC_LEFT and
173            // ANCENSTRY_PATH if they are set, but they have no purpose here.
174            let flags = WalkFlags::empty();
175            (flags, WalkFlags::Seen)
176        };
177
178        for (id, _) in parents {
179            self.states.entry(*id).and_modify(|s| *s |= pass).or_insert(insert);
180        }
181        Ok(())
182    }
183
184    fn collect_parents(&mut self, id: &oid) -> Result<SmallVec<[(ObjectId, GenAndCommitTime); 1]>, Error> {
185        collect_parents(
186            &mut self.commit_graph,
187            &self.find,
188            id,
189            matches!(self.parents, Parents::First),
190            &mut self.buf,
191        )
192    }
193
194    // Same as collect_parents but disregards the first_parent flag
195    pub(super) fn collect_all_parents(
196        &mut self,
197        id: &oid,
198    ) -> Result<SmallVec<[(ObjectId, GenAndCommitTime); 1]>, Error> {
199        collect_parents(&mut self.commit_graph, &self.find, id, false, &mut self.buf)
200    }
201
202    fn pop_commit(&mut self) -> Option<Result<Info, Error>> {
203        let commit = self.topo_queue.pop()?;
204        let i = match self.indegrees.get_mut(&commit.id) {
205            Some(i) => i,
206            None => {
207                return Some(Err(Error::MissingIndegreeUnexpected));
208            }
209        };
210
211        *i = 0;
212        if let Err(e) = self.expand_topo_walk(&commit.id) {
213            return Some(Err(e));
214        }
215
216        Some(Ok(commit))
217    }
218}
219
220impl<Find, Predicate> Iterator for Topo<Find, Predicate>
221where
222    Find: gix_object::Find,
223    Predicate: FnMut(&oid) -> bool,
224{
225    type Item = Result<Info, Error>;
226
227    fn next(&mut self) -> Option<Self::Item> {
228        loop {
229            match self.pop_commit()? {
230                Ok(id) => {
231                    if (self.predicate)(&id.id) {
232                        return Some(Ok(id));
233                    }
234                }
235                Err(e) => return Some(Err(e)),
236            }
237        }
238    }
239}
240
241fn collect_parents<Find>(
242    cache: &mut Option<gix_commitgraph::Graph>,
243    f: Find,
244    id: &oid,
245    first_only: bool,
246    buf: &mut Vec<u8>,
247) -> Result<SmallVec<[(ObjectId, GenAndCommitTime); 1]>, Error>
248where
249    Find: gix_object::Find,
250{
251    let mut parents = SmallVec::<[(ObjectId, GenAndCommitTime); 1]>::new();
252    match find(cache.as_ref(), &f, id, buf)? {
253        Either::CommitRefIter(c) => {
254            for token in c {
255                use gix_object::commit::ref_iter::Token as T;
256                match token {
257                    Ok(T::Tree { .. }) => continue,
258                    Ok(T::Parent { id }) => {
259                        parents.push((id, (0, 0))); // Dummy numbers to be filled in
260                        if first_only {
261                            break;
262                        }
263                    }
264                    Ok(_past_parents) => break,
265                    Err(err) => return Err(err.into()),
266                }
267            }
268            // Need to check the cache again. That a commit is not in the cache
269            // doesn't mean a parent is not.
270            for (id, gen_time) in parents.iter_mut() {
271                let commit = find(cache.as_ref(), &f, id, buf)?;
272                *gen_time = gen_and_commit_time(commit)?;
273            }
274        }
275        Either::CachedCommit(c) => {
276            for pos in c.iter_parents() {
277                let Ok(pos) = pos else {
278                    // drop corrupt cache and use ODB from now on.
279                    *cache = None;
280                    return collect_parents(cache, f, id, first_only, buf);
281                };
282                let parent_commit = cache
283                    .as_ref()
284                    .expect("cache exists if CachedCommit was returned")
285                    .commit_at(pos);
286                parents.push((
287                    parent_commit.id().into(),
288                    (parent_commit.generation(), parent_commit.committer_timestamp() as i64),
289                ));
290                if first_only {
291                    break;
292                }
293            }
294        }
295    }
296    Ok(parents)
297}
298
299pub(super) fn gen_and_commit_time(c: Either<'_, '_>) -> Result<GenAndCommitTime, Error> {
300    match c {
301        Either::CommitRefIter(c) => {
302            let mut commit_time = 0;
303            for token in c {
304                use gix_object::commit::ref_iter::Token as T;
305                match token {
306                    Ok(T::Tree { .. }) => continue,
307                    Ok(T::Parent { .. }) => continue,
308                    Ok(T::Author { .. }) => continue,
309                    Ok(T::Committer { signature }) => {
310                        commit_time = signature.seconds();
311                        break;
312                    }
313                    Ok(_unused_token) => break,
314                    Err(err) => return Err(err.into()),
315                }
316            }
317            Ok((gix_commitgraph::GENERATION_NUMBER_INFINITY, commit_time))
318        }
319        Either::CachedCommit(c) => Ok((c.generation(), c.committer_timestamp() as i64)),
320    }
321}