gix_traverse/commit/topo/
init.rs

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