Show More
@@ -0,0 +1,196 b'' | |||||
|
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 b'' | |||||
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