gix_traverse/commit/topo/
init.rs

1use gix_hash::{oid, ObjectId};
2use gix_revwalk::{graph::IdMap, PriorityQueue};
3
4use crate::commit::{
5    find,
6    topo::{iter::gen_and_commit_time, Error, Sorting, WalkFlags},
7    Info, Parents, Topo,
8};
9
10/// Builder for [`Topo`].
11pub struct Builder<Find, Predicate> {
12    commit_graph: Option<gix_commitgraph::Graph>,
13    find: Find,
14    predicate: Predicate,
15    sorting: Sorting,
16    parents: Parents,
17    tips: Vec<ObjectId>,
18    ends: Vec<ObjectId>,
19}
20
21impl<Find> Builder<Find, fn(&oid) -> bool>
22where
23    Find: gix_object::Find,
24{
25    /// Create a new `Builder` for a [`Topo`] that reads commits from a repository with `find`.
26    /// starting at the `tips` and ending at the `ends`. Like `git rev-list
27    /// --topo-order ^ends tips`.
28    pub fn from_iters(
29        find: Find,
30        tips: impl IntoIterator<Item = impl Into<ObjectId>>,
31        ends: Option<impl IntoIterator<Item = impl Into<ObjectId>>>,
32    ) -> Self {
33        Self::new(find).with_tips(tips).with_ends(ends.into_iter().flatten())
34    }
35
36    /// Create a new `Builder` for a [`Topo`] that reads commits from a
37    /// repository with `find`.
38    pub fn new(find: Find) -> Self {
39        Self {
40            commit_graph: Default::default(),
41            find,
42            sorting: Default::default(),
43            parents: Default::default(),
44            tips: Default::default(),
45            ends: Default::default(),
46            predicate: |_| true,
47        }
48    }
49
50    /// Set a `predicate` to filter out revisions from the walk. Can be used to
51    /// implement e.g. filtering on paths or time. This does *not* exclude the
52    /// parent(s) of a revision that is excluded. Specify a revision as an 'end'
53    /// if you want that behavior.
54    pub fn with_predicate<Predicate>(self, predicate: Predicate) -> Builder<Find, Predicate>
55    where
56        Predicate: FnMut(&oid) -> bool,
57    {
58        Builder {
59            commit_graph: self.commit_graph,
60            find: self.find,
61            sorting: self.sorting,
62            parents: self.parents,
63            tips: self.tips,
64            ends: self.ends,
65            predicate,
66        }
67    }
68}
69
70impl<Find, Predicate> Builder<Find, Predicate>
71where
72    Find: gix_object::Find,
73    Predicate: FnMut(&oid) -> bool,
74{
75    /// Add commits to start reading from.
76    ///
77    /// The behavior is similar to specifying additional `ends` in `git rev-list --topo-order ^ends tips`.
78    pub fn with_tips(mut self, tips: impl IntoIterator<Item = impl Into<ObjectId>>) -> Self {
79        self.tips.extend(tips.into_iter().map(Into::into));
80        self
81    }
82
83    /// Add commits ending the traversal.
84    ///
85    /// These commits themselves will not be read, i.e. the behavior is similar to specifying additional
86    /// `ends` in `git rev-list --topo-order ^ends tips`.
87    pub fn with_ends(mut self, ends: impl IntoIterator<Item = impl Into<ObjectId>>) -> Self {
88        self.ends.extend(ends.into_iter().map(Into::into));
89        self
90    }
91
92    /// Set the `sorting` to use for the topological walk.
93    pub fn sorting(mut self, sorting: Sorting) -> Self {
94        self.sorting = sorting;
95        self
96    }
97
98    /// Specify how to handle commit `parents` during traversal.
99    pub fn parents(mut self, parents: Parents) -> Self {
100        self.parents = parents;
101        self
102    }
103
104    /// Set or unset the `commit_graph` to use for the iteration.
105    pub fn with_commit_graph(mut self, commit_graph: Option<gix_commitgraph::Graph>) -> Self {
106        self.commit_graph = commit_graph;
107        self
108    }
109
110    /// Build a new [`Topo`] instance.
111    ///
112    /// Note that merely building an instance is currently expensive.
113    pub fn build(self) -> Result<Topo<Find, Predicate>, Error> {
114        let mut w = Topo {
115            commit_graph: self.commit_graph,
116            find: self.find,
117            predicate: self.predicate,
118            indegrees: IdMap::default(),
119            states: IdMap::default(),
120            explore_queue: PriorityQueue::new(),
121            indegree_queue: PriorityQueue::new(),
122            topo_queue: super::iter::Queue::new(self.sorting),
123            parents: self.parents,
124            min_gen: gix_commitgraph::GENERATION_NUMBER_INFINITY,
125            buf: vec![],
126        };
127
128        // Initial flags for the states of the tips and ends. All of them are
129        // seen and added to the explore and indegree queues. The ends are by
130        // definition (?) uninteresting and bottom.
131        let tip_flags = WalkFlags::Seen | WalkFlags::Explored | WalkFlags::InDegree;
132        let end_flags = tip_flags | WalkFlags::Uninteresting | WalkFlags::Bottom;
133
134        for (id, flags) in self
135            .tips
136            .iter()
137            .map(|id| (id, tip_flags))
138            .chain(self.ends.iter().map(|id| (id, end_flags)))
139        {
140            *w.indegrees.entry(*id).or_default() = 1;
141            let commit = find(w.commit_graph.as_ref(), &w.find, id, &mut w.buf)?;
142            let (gen, time) = gen_and_commit_time(commit)?;
143
144            if gen < w.min_gen {
145                w.min_gen = gen;
146            }
147
148            w.states.insert(*id, flags);
149            w.explore_queue.insert((gen, time), *id);
150            w.indegree_queue.insert((gen, time), *id);
151        }
152
153        // NOTE: Parents of the ends must also be marked uninteresting for some
154        // reason. See handle_commit()
155        for id in &self.ends {
156            let parents = w.collect_all_parents(id)?;
157            for (id, _) in parents {
158                w.states
159                    .entry(id)
160                    .and_modify(|s| *s |= WalkFlags::Uninteresting)
161                    .or_insert(WalkFlags::Uninteresting | WalkFlags::Seen);
162            }
163        }
164
165        w.compute_indegrees_to_depth(w.min_gen)?;
166
167        // NOTE: in Git the ends are also added to the topo_queue in addition to
168        // the tips, but then in simplify_commit() Git is told to ignore it. For
169        // now the tests pass.
170        for id in self.tips.iter() {
171            let i = w.indegrees.get(id).ok_or(Error::MissingIndegreeUnexpected)?;
172
173            if *i != 1 {
174                continue;
175            }
176
177            let commit = find(w.commit_graph.as_ref(), &w.find, id, &mut w.buf)?;
178            let (_, time) = gen_and_commit_time(commit)?;
179            let parent_ids = w.collect_all_parents(id)?.into_iter().map(|e| e.0).collect();
180
181            w.topo_queue.push(
182                time,
183                Info {
184                    id: *id,
185                    parent_ids,
186                    commit_time: Some(time),
187                },
188            );
189        }
190
191        w.topo_queue.initial_sort();
192        Ok(w)
193    }
194}