Coverage Report

Created: 2026-08-05 07:37

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