1use ahash::{HashMap, HashSet, HashSetExt as _};
20use blockworx_doc::{
21 block_model::Live,
22 commit::CommitBuilder,
23 document::Document,
24 entity::Entity,
25 opcode::{Crud, OpCodes},
26};
27
28pub fn label(rev: blockworx_doc::rev::Rev) -> String {
32 format!("Restore rev {}", rev.get())
33}
34
35pub fn restore(head: &Document, target: &Document, builder: &mut CommitBuilder) {
38 for (hash, asset) in target.assets() {
42 if head.asset(&hash).is_none() {
43 builder.push(OpCodes::Asset(hash, asset.clone()));
44 }
45 }
46 for update in head
47 .title_block()
48 .updates_toward(&target.title_block().to_init())
49 {
50 builder.push(OpCodes::Document(update));
51 }
52 kind(head.blocks(), target.blocks(), OpCodes::Block, builder);
53 kind(head.pins(), target.pins(), OpCodes::Pin, builder);
54 kind(head.routes(), target.routes(), OpCodes::Route, builder);
55 kind(
56 head.route_labels(),
57 target.route_labels(),
58 OpCodes::RouteLabel,
59 builder,
60 );
61 kind(head.texts(), target.texts(), OpCodes::Text, builder);
62 kind(head.areas(), target.areas(), OpCodes::Area, builder);
63 kind(head.images(), target.images(), OpCodes::Image, builder);
64}
65
66fn kind<'a, I, E: Entity + 'a>(
69 head: impl Iterator<Item = (I, &'a Live<E>)>,
70 target: impl Iterator<Item = (I, &'a Live<E>)>,
71 op: impl Fn(I, Crud<E::Init, E::Update>) -> OpCodes,
72 builder: &mut CommitBuilder,
73) where
74 I: Copy + Eq + std::hash::Hash + Ord,
75{
76 let head: HashMap<I, &Live<E>> = head.collect();
77 let mut target: Vec<(I, &Live<E>)> = target.collect();
78 target.sort_unstable_by_key(|(id, _)| *id);
79 let mut seen = HashSet::with_capacity(target.len());
80
81 for (id, want) in target {
82 seen.insert(id);
83 match (head.get(&id), want.is_alive()) {
84 (None, true) => builder.push(op(id, Crud::Create(want.as_ref().to_init()))),
88 (None, false) => {}
89 (Some(have), true) => {
90 if !have.is_alive() {
91 builder.push(op(id, Crud::Restore));
92 }
93 let init = want.as_ref().to_init();
94 for update in have.as_ref().updates_toward(&init) {
95 builder.push(op(id, Crud::Update(update)));
96 }
97 }
98 (Some(have), false) => {
99 if have.is_alive() {
100 builder.push(op(id, Crud::Delete));
101 }
102 }
103 }
104 }
105 let mut born_since: Vec<I> = head
107 .iter()
108 .filter(|(id, live)| live.is_alive() && !seen.contains(*id))
109 .map(|(id, _)| *id)
110 .collect();
111 born_since.sort_unstable();
112 for id in born_since {
113 builder.push(op(id, Crud::Delete));
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::edit::harness as h;
121 use crate::store::tests::fixture as fx_store;
122 use blockworx_doc::{
123 commit::Commit,
124 fixtures::{block_id, pin_id},
125 repo::Repo,
126 };
127
128 fn projected(repo: &Repo) -> String {
133 crate::schema::project::to_json(repo)
134 }
135
136 fn restore_to_prefix(commits: &[Commit], prefix: usize) -> (Repo, Repo) {
139 let head = Repo::folding(commits).expect("the history folds");
140 let target = Repo::folding(&commits[..prefix]).expect("the prefix folds");
141 let mut builder = CommitBuilder::new(label(target.rev()));
142 restore(head.document(), target.document(), &mut builder);
143 let mut restored = head;
144 if let Some(commit) = builder.seal() {
145 restored.submit(commit).expect("the restore folds");
146 }
147 (restored, target)
148 }
149
150 fn assert_restores(commits: &[Commit], prefix: usize) {
151 let (restored, target) = restore_to_prefix(commits, prefix);
152 assert_eq!(
153 projected(&restored),
154 projected(&target),
155 "the restored head is not the document rev {} held",
156 target.rev().get(),
157 );
158 }
159
160 fn block(byte: u8, name: &str) -> Commit {
161 fx_store::commit("Added a block", vec![fx_store::block_create(byte, name)])
162 }
163
164 fn history() -> Vec<Commit> {
168 vec![
169 block(1, "Adder"),
170 block(2, "Summer"),
171 fx_store::commit("Renamed it", vec![fx_store::block_rename(1, "Renamed")]),
173 fx_store::commit(
174 "Deleted it",
175 vec![OpCodes::Block(block_id(2), Crud::Delete)],
176 ),
177 block(3, "Born later"),
178 ]
179 }
180
181 #[test]
182 fn a_restore_carries_deletes_creates_and_renames_back_together() {
183 let commits = history();
184 let (restored, target) = restore_to_prefix(&commits, 2);
185
186 assert_eq!(
187 restored.log().len(),
188 commits.len() + 1,
189 "a restore is one forward commit, never a rewrite",
190 );
191 assert!(
192 restored
193 .document()
194 .block(&block_id(2))
195 .expect("the deleted block is still an entity")
196 .is_alive(),
197 "the block deleted after the rev did not come back",
198 );
199 assert!(
200 !restored
201 .document()
202 .block(&blockworx_doc::fixtures::block_id(3))
203 .expect("the later block is still an entity")
204 .is_alive(),
205 "the block born after the rev was not tombstoned",
206 );
207 assert_eq!(
208 restored
209 .document()
210 .block(&block_id(1))
211 .expect("the surviving block")
212 .as_ref()
213 .title
214 .name
215 .as_ref(),
216 "Adder",
217 "the rename made after the rev was not carried back",
218 );
219 assert_eq!(projected(&restored), projected(&target));
220 }
221
222 #[test]
225 fn every_rev_of_a_history_restores_to_itself() {
226 let commits = history();
227 for prefix in 0..=commits.len() {
228 assert_restores(&commits, prefix);
229 }
230 }
231
232 #[test]
236 fn restoring_the_head_seals_to_nothing() {
237 let head = Repo::folding(&history()).expect("the history folds");
238 let mut builder = CommitBuilder::new("Restore".to_owned());
239 restore(head.document(), head.document(), &mut builder);
240 h::seals_to_nothing(builder);
241 }
242
243 #[test]
247 fn a_wire_and_its_endpoints_come_back_in_one_commit() {
248 use crate::path::Scope;
249 use crate::widget::test_fixtures as fx;
250 use blockworx_doc::values::PinSide;
251
252 let commits = vec![
253 fx_store::commit(
254 "Built a scene",
255 vec![
256 fx::block_in(
257 1,
258 Scope::Root,
259 egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(60.0, 150.0)),
260 ),
261 fx::block_in(
262 2,
263 Scope::Root,
264 egui::Rect::from_min_max(egui::pos2(180.0, 0.0), egui::pos2(240.0, 150.0)),
265 ),
266 fx::pin(3, 1, PinSide::East, 0),
267 fx::pin(4, 2, PinSide::West, 0),
268 fx::route(5, Scope::Root, 3, 4, &[(20, 4)]),
269 ],
270 ),
271 fx_store::commit(
274 "Deleted a pin and its wire",
275 vec![
276 OpCodes::Route(blockworx_doc::fixtures::route_id(5), Crud::Delete),
277 OpCodes::Pin(pin_id(3), Crud::Delete),
278 ],
279 ),
280 ];
281 let (restored, target) = restore_to_prefix(&commits, 1);
282 assert!(
283 restored
284 .document()
285 .route(&blockworx_doc::fixtures::route_id(5))
286 .expect("the route is still an entity")
287 .is_alive(),
288 "the wire did not come back with its pin",
289 );
290 assert_eq!(projected(&restored), projected(&target));
291 }
292}