##// END OF EJS Templates
rust-discovery: starting core implementation...
Georges Racinet -
r42355:10b465d6 default
parent child Browse files
Show More
@@ -0,0 +1,196
1 // discovery.rs
2 //
3 // Copyright 2019 Georges Racinet <georges.racinet@octobus.net>
4 //
5 // This software may be used and distributed according to the terms of the
6 // GNU General Public License version 2 or any later version.
7
8 //! Discovery operations
9 //!
10 //! This is a Rust counterpart to the `partialdiscovery` class of
11 //! `mercurial.setdiscovery`
12
13 use super::{Graph, GraphError, Revision};
14 use crate::ancestors::MissingAncestors;
15 use crate::dagops;
16 use std::collections::HashSet;
17
18 pub struct PartialDiscovery<G: Graph + Clone> {
19 target_heads: Option<Vec<Revision>>,
20 graph: G, // plays the role of self._repo
21 common: MissingAncestors<G>,
22 undecided: Option<HashSet<Revision>>,
23 missing: HashSet<Revision>,
24 }
25
26 impl<G: Graph + Clone> PartialDiscovery<G> {
27 /// Create a PartialDiscovery object, with the intent
28 /// of comparing our `::<target_heads>` revset to the contents of another
29 /// repo.
30 ///
31 /// For now `target_heads` is passed as a vector, and will be used
32 /// at the first call to `ensure_undecided()`.
33 ///
34 /// If we want to make the signature more flexible,
35 /// we'll have to make it a type argument of `PartialDiscovery` or a trait
36 /// object since we'll keep it in the meanwhile
37 pub fn new(graph: G, target_heads: Vec<Revision>) -> Self {
38 PartialDiscovery {
39 undecided: None,
40 target_heads: Some(target_heads),
41 graph: graph.clone(),
42 common: MissingAncestors::new(graph, vec![]),
43 missing: HashSet::new(),
44 }
45 }
46
47 /// Register revisions known as being common
48 pub fn add_common_revisions(
49 &mut self,
50 common: impl IntoIterator<Item = Revision>,
51 ) -> Result<(), GraphError> {
52 self.common.add_bases(common);
53 if let Some(ref mut undecided) = self.undecided {
54 self.common.remove_ancestors_from(undecided)?;
55 }
56 Ok(())
57 }
58
59 /// Register revisions known as being missing
60 pub fn add_missing_revisions(
61 &mut self,
62 missing: impl IntoIterator<Item = Revision>,
63 ) -> Result<(), GraphError> {
64 self.ensure_undecided()?;
65 let range = dagops::range(
66 &self.graph,
67 missing,
68 self.undecided.as_ref().unwrap().iter().cloned(),
69 )?;
70 let undecided_mut = self.undecided.as_mut().unwrap();
71 for missrev in range {
72 self.missing.insert(missrev);
73 undecided_mut.remove(&missrev);
74 }
75 Ok(())
76 }
77
78 /// Do we have any information about the peer?
79 pub fn has_info(&self) -> bool {
80 self.common.has_bases()
81 }
82
83 /// Did we acquire full knowledge of our Revisions that the peer has?
84 pub fn is_complete(&self) -> bool {
85 self.undecided.as_ref().map_or(false, |s| s.is_empty())
86 }
87
88 /// Return the heads of the currently known common set of revisions.
89 ///
90 /// If the discovery process is not complete (see `is_complete()`), the
91 /// caller must be aware that this is an intermediate state.
92 ///
93 /// On the other hand, if it is complete, then this is currently
94 /// the only way to retrieve the end results of the discovery process.
95 ///
96 /// We may introduce in the future an `into_common_heads` call that
97 /// would be more appropriate for normal Rust callers, dropping `self`
98 /// if it is complete.
99 pub fn common_heads(&self) -> Result<HashSet<Revision>, GraphError> {
100 self.common.bases_heads()
101 }
102
103 /// Force first computation of `self.undecided`
104 ///
105 /// After this, `self.undecided.as_ref()` and `.as_mut()` can be
106 /// unwrapped to get workable immutable or mutable references without
107 /// any panic.
108 ///
109 /// This is an imperative call instead of an access with added lazyness
110 /// to reduce easily the scope of mutable borrow for the caller,
111 /// compared to undecided(&'a mut self) -> &'a… that would keep it
112 /// as long as the resulting immutable one.
113 fn ensure_undecided(&mut self) -> Result<(), GraphError> {
114 if self.undecided.is_some() {
115 return Ok(());
116 }
117 let tgt = self.target_heads.take().unwrap();
118 self.undecided =
119 Some(self.common.missing_ancestors(tgt)?.into_iter().collect());
120 Ok(())
121 }
122 }
123
124 #[cfg(test)]
125 mod tests {
126 use super::*;
127 use crate::testing::SampleGraph;
128
129 /// A PartialDiscovery as for pushing all the heads of `SampleGraph`
130 fn full_disco() -> PartialDiscovery<SampleGraph> {
131 PartialDiscovery::new(SampleGraph, vec![10, 11, 12, 13])
132 }
133
134 fn sorted_undecided(
135 disco: &PartialDiscovery<SampleGraph>,
136 ) -> Vec<Revision> {
137 let mut as_vec: Vec<Revision> =
138 disco.undecided.as_ref().unwrap().iter().cloned().collect();
139 as_vec.sort();
140 as_vec
141 }
142
143 fn sorted_missing(disco: &PartialDiscovery<SampleGraph>) -> Vec<Revision> {
144 let mut as_vec: Vec<Revision> =
145 disco.missing.iter().cloned().collect();
146 as_vec.sort();
147 as_vec
148 }
149
150 fn sorted_common_heads(
151 disco: &PartialDiscovery<SampleGraph>,
152 ) -> Result<Vec<Revision>, GraphError> {
153 let mut as_vec: Vec<Revision> =
154 disco.common_heads()?.iter().cloned().collect();
155 as_vec.sort();
156 Ok(as_vec)
157 }
158
159 #[test]
160 fn test_add_common_get_undecided() -> Result<(), GraphError> {
161 let mut disco = full_disco();
162 assert_eq!(disco.undecided, None);
163 assert!(!disco.has_info());
164
165 disco.add_common_revisions(vec![11, 12])?;
166 assert!(disco.has_info());
167 assert!(!disco.is_complete());
168 assert!(disco.missing.is_empty());
169
170 // add_common_revisions did not trigger a premature computation
171 // of `undecided`, let's check that and ask for them
172 assert_eq!(disco.undecided, None);
173 disco.ensure_undecided()?;
174 assert_eq!(sorted_undecided(&disco), vec![5, 8, 10, 13]);
175 Ok(())
176 }
177
178 /// in this test, we pretend that our peer misses exactly (8+10)::
179 /// and we're comparing all our repo to it (as in a bare push)
180 #[test]
181 fn test_discovery() -> Result<(), GraphError> {
182 let mut disco = full_disco();
183 disco.add_common_revisions(vec![11, 12])?;
184 disco.add_missing_revisions(vec![8, 10])?;
185 assert_eq!(sorted_undecided(&disco), vec![5]);
186 assert_eq!(sorted_missing(&disco), vec![8, 10, 13]);
187 assert!(!disco.is_complete());
188
189 disco.add_common_revisions(vec![5])?;
190 assert_eq!(sorted_undecided(&disco), vec![]);
191 assert_eq!(sorted_missing(&disco), vec![8, 10, 13]);
192 assert!(disco.is_complete());
193 assert_eq!(sorted_common_heads(&disco)?, vec![5, 11, 12]);
194 Ok(())
195 }
196 }
@@ -1,41 +1,42
1 // Copyright 2018 Georges Racinet <gracinet@anybox.fr>
1 // Copyright 2018 Georges Racinet <gracinet@anybox.fr>
2 //
2 //
3 // This software may be used and distributed according to the terms of the
3 // This software may be used and distributed according to the terms of the
4 // GNU General Public License version 2 or any later version.
4 // GNU General Public License version 2 or any later version.
5 mod ancestors;
5 mod ancestors;
6 pub mod dagops;
6 pub mod dagops;
7 pub use ancestors::{AncestorsIterator, LazyAncestors, MissingAncestors};
7 pub use ancestors::{AncestorsIterator, LazyAncestors, MissingAncestors};
8 pub mod testing; // unconditionally built, for use from integration tests
8 pub mod testing; // unconditionally built, for use from integration tests
9 pub mod discovery;
9
10
10 /// Mercurial revision numbers
11 /// Mercurial revision numbers
11 ///
12 ///
12 /// As noted in revlog.c, revision numbers are actually encoded in
13 /// As noted in revlog.c, revision numbers are actually encoded in
13 /// 4 bytes, and are liberally converted to ints, whence the i32
14 /// 4 bytes, and are liberally converted to ints, whence the i32
14 pub type Revision = i32;
15 pub type Revision = i32;
15
16
16
17
17 /// Marker expressing the absence of a parent
18 /// Marker expressing the absence of a parent
18 ///
19 ///
19 /// Independently of the actual representation, `NULL_REVISION` is guaranteed
20 /// Independently of the actual representation, `NULL_REVISION` is guaranteed
20 /// to be smaller that all existing revisions.
21 /// to be smaller that all existing revisions.
21 pub const NULL_REVISION: Revision = -1;
22 pub const NULL_REVISION: Revision = -1;
22
23
23 /// Same as `mercurial.node.wdirrev`
24 /// Same as `mercurial.node.wdirrev`
24 ///
25 ///
25 /// This is also equal to `i32::max_value()`, but it's better to spell
26 /// This is also equal to `i32::max_value()`, but it's better to spell
26 /// it out explicitely, same as in `mercurial.node`
27 /// it out explicitely, same as in `mercurial.node`
27 pub const WORKING_DIRECTORY_REVISION: Revision = 0x7fffffff;
28 pub const WORKING_DIRECTORY_REVISION: Revision = 0x7fffffff;
28
29
29 /// The simplest expression of what we need of Mercurial DAGs.
30 /// The simplest expression of what we need of Mercurial DAGs.
30 pub trait Graph {
31 pub trait Graph {
31 /// Return the two parents of the given `Revision`.
32 /// Return the two parents of the given `Revision`.
32 ///
33 ///
33 /// Each of the parents can be independently `NULL_REVISION`
34 /// Each of the parents can be independently `NULL_REVISION`
34 fn parents(&self, Revision) -> Result<[Revision; 2], GraphError>;
35 fn parents(&self, Revision) -> Result<[Revision; 2], GraphError>;
35 }
36 }
36
37
37 #[derive(Clone, Debug, PartialEq)]
38 #[derive(Clone, Debug, PartialEq)]
38 pub enum GraphError {
39 pub enum GraphError {
39 ParentOutOfRange(Revision),
40 ParentOutOfRange(Revision),
40 WorkingDirectoryUnsupported,
41 WorkingDirectoryUnsupported,
41 }
42 }
General Comments 0
You need to be logged in to leave comments. Login now