|
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 } |