1use std::collections::BTreeSet;
28use std::path::Path;
29
30use blockworx_doc::{hash::AssetHash, rev::Rev};
31
32use super::assets::AssetFormat;
33use super::container::{ASSETS, Container, ContainerError, MANIFEST};
34use super::handle::{Clock, Store};
35use super::manifest::{Replays, Row};
36use super::record::Identity;
37use super::revs;
38use super::tags::{Tagging, Tags};
39
40#[derive(Debug, thiserror::Error)]
42pub enum PrefixFailure {
43 #[error("the manifest could not be read: {0}")]
44 Read(std::io::Error),
45 #[error("line {line} of the manifest is not a row this build can read: {why}")]
46 NotARow { line: usize, why: serde_json::Error },
47 #[error("this history has no rev {}", .0.get())]
48 NoSuchRev(Rev),
49 #[error(transparent)]
50 Create(#[from] ContainerError),
51 #[error("{name} could not be carried over: {why}")]
52 Asset { name: String, why: std::io::Error },
53 #[error("rev {}'s copy could not be carried over: {why}", .at.get())]
54 Rev { at: Rev, why: std::io::Error },
55 #[error("the new container's projection did not land: {0}")]
56 Projection(super::Refusal),
57 #[error("rev {}'s name did not come over: {why}", .at.get())]
58 Tag { at: Rev, why: super::Refusal },
59}
60
61pub fn save_through(
72 source: &Path,
73 at: Rev,
74 to: &Path,
75 mut clock: Clock,
76 by: &Identity,
77) -> Result<Store, PrefixFailure> {
78 let text = std::fs::read_to_string(source.join(MANIFEST)).map_err(PrefixFailure::Read)?;
79 let prefix = through(&text, at)?;
80 let container = Container::create_holding(to, clock.tick(), prefix.as_bytes())?;
81 carry_revs(source, to, at)?;
82 carry_assets(source, to, at)?;
83 let mut store = Store::over(container, clock)?;
84 rename_revs(&mut store, &named_through(&text, at)?, by)?;
85 store.save_projection().map_err(PrefixFailure::Projection)?;
86 Ok(store)
87}
88
89fn rename_revs(store: &mut Store, wanted: &Tags, by: &Identity) -> Result<(), PrefixFailure> {
96 let carried: Vec<(Rev, Vec<String>)> = store
97 .tags()
98 .iter()
99 .map(|(rev, names)| (rev, names.to_vec()))
100 .collect();
101 let mut restated: Vec<(Rev, String, Tagging)> = Vec::new();
102 for (rev, names) in &carried {
103 for name in names {
104 if !wanted.of(*rev).contains(name) {
105 restated.push((*rev, name.clone(), Tagging::Removed));
106 }
107 }
108 }
109 for (rev, names) in wanted.iter() {
110 for name in names {
111 if !store.tags().of(rev).contains(name) {
112 restated.push((rev, name.clone(), Tagging::Added));
113 }
114 }
115 }
116 for (rev, name, how) in restated {
117 store
118 .tag(rev, &name, how, by)
119 .map_err(|why| PrefixFailure::Tag { at: rev, why })?;
120 }
121 Ok(())
122}
123
124fn named_through(text: &str, at: Rev) -> Result<Tags, PrefixFailure> {
129 let mut tags = Tags::default();
130 for (nth, line) in whole_lines(text) {
131 let row = parse(nth, line)?;
132 if row.rev <= at
133 && let Replays::Vocabulary(how) = row.kind.replays()
134 {
135 tags.apply(row.rev, &row.label, how);
136 }
137 }
138 Ok(tags)
139}
140
141fn through(text: &str, at: Rev) -> Result<&str, PrefixFailure> {
151 let mut end = 0;
152 let mut reached = Rev::ZERO;
153 for (nth, line) in whole_lines(text) {
154 let row = parse(nth, line)?;
155 if row.kind.takes_a_rev() {
156 if row.rev > at {
157 break;
158 }
159 reached = row.rev;
160 }
161 end += line.len();
162 }
163 if reached != at {
164 return Err(PrefixFailure::NoSuchRev(at));
165 }
166 Ok(&text[..end])
167}
168
169fn parse(nth: usize, line: &str) -> Result<Row, PrefixFailure> {
170 serde_json::from_str(line.trim_end())
171 .map_err(|why| PrefixFailure::NotARow { line: nth + 1, why })
172}
173
174fn whole_lines(text: &str) -> impl Iterator<Item = (usize, &str)> {
178 text.split_inclusive('\n')
179 .take_while(|line| line.ends_with('\n'))
180 .enumerate()
181}
182
183fn carry_revs(from: &Path, to: &Path, at: Rev) -> Result<(), PrefixFailure> {
186 for rev in revs::through(at) {
187 std::fs::copy(revs::path(from, rev), revs::path(to, rev))
188 .map_err(|why| PrefixFailure::Rev { at: rev, why })?;
189 }
190 Ok(())
191}
192
193fn carry_assets(from: &Path, to: &Path, at: Rev) -> Result<(), PrefixFailure> {
202 let backing = revs::Dir::at(to);
203 let mut wanted: BTreeSet<AssetHash> = BTreeSet::new();
204 for rev in revs::through(at) {
205 let Ok(document) = revs::read(&backing, rev) else {
206 continue;
207 };
208 wanted.extend(document.referenced_assets());
209 }
210 for name in wanted
211 .into_iter()
212 .flat_map(|hash| AssetFormat::ALL.map(move |format| format.file_name(hash)))
213 {
214 let (source, target) = (from.join(ASSETS).join(&name), to.join(ASSETS).join(&name));
215 if target.exists() || !source.exists() {
216 continue;
217 }
218 std::fs::copy(&source, &target).map_err(|why| PrefixFailure::Asset { name, why })?;
219 }
220 Ok(())
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::store::record::Identity;
227 use crate::store::tests::fixture;
228 use blockworx_doc::fixtures::rev;
229
230 fn source(root: &Path) -> Store {
233 let mut store = Store::create(root, fixture::clock()).expect("the container");
234 let author = Identity::new("ada");
235 for (n, commit) in fixture::edits(3).iter().enumerate() {
236 store
237 .submit_edit(commit.clone(), &author)
238 .expect("the edit lands");
239 if n == 0 {
240 store
241 .tag(rev(1), "the start", Tagging::Added, &author)
242 .expect("the tag");
243 }
244 }
245 store
246 .tag(rev(3), "the end", Tagging::Added, &author)
247 .expect("the tag");
248 store
249 }
250
251 #[test]
255 fn the_saved_log_is_the_sources_own_lines() {
256 let dir = fixture::dir("prefix-bytes");
257 let from = dir.join("source.bwx");
258 drop(source(&from));
259 let to = dir.join("through-2.bwx");
260
261 let saved = save_through(
262 &from,
263 rev(2),
264 &to,
265 fixture::clock(),
266 &Identity::new("grace"),
267 )
268 .expect("the prefix saves");
269 drop(saved);
270
271 let whole = std::fs::read_to_string(from.join(MANIFEST)).expect("the source manifest");
272 let prefix = std::fs::read_to_string(to.join(MANIFEST)).expect("the saved manifest");
273 assert!(
274 whole.starts_with(&prefix),
275 "the saved log is not a prefix of the one it came from",
276 );
277 assert_eq!(
278 prefix.lines().count(),
279 3,
280 "expected two edits and the tag between them, not {prefix}",
281 );
282 assert!(
283 whole.len() > prefix.len(),
284 "precondition: the source runs past rev 2",
285 );
286 }
287
288 #[test]
292 fn the_saved_container_carries_exactly_the_revs_through_the_cut() {
293 let dir = fixture::dir("prefix-revs");
294 let from = dir.join("source.bwx");
295 drop(source(&from));
296 let to = dir.join("through-2.bwx");
297 assert!(
298 revs::path(&from, rev(3)).is_file(),
299 "precondition: the source holds a rev past the cut",
300 );
301
302 let saved = save_through(
303 &from,
304 rev(2),
305 &to,
306 fixture::clock(),
307 &Identity::new("grace"),
308 )
309 .expect("the prefix saves");
310 drop(saved);
311
312 for at in revs::through(rev(2)) {
313 assert_eq!(
314 std::fs::read(revs::path(&from, at)).expect("the source rev"),
315 std::fs::read(revs::path(&to, at)).expect("the saved rev"),
316 "rev {} did not come over as the bytes it was",
317 at.get(),
318 );
319 }
320 assert!(
321 !revs::path(&to, rev(3)).exists(),
322 "a rev past the cut came over with the save",
323 );
324 }
325
326 #[test]
329 fn the_saved_container_replays_to_the_rev_it_was_cut_at() {
330 let dir = fixture::dir("prefix-replay");
331 let from = dir.join("source.bwx");
332 let held = source(&from);
333 let at_two = blockworx_doc::repo::Repo::folding(&held.repo().log()[..2])
334 .expect("the prefix folds")
335 .document()
336 .clone();
337 drop(held);
338
339 let to = dir.join("through-2.bwx");
340 let saved = save_through(
341 &from,
342 rev(2),
343 &to,
344 fixture::clock(),
345 &Identity::new("grace"),
346 )
347 .expect("the prefix saves");
348 assert_eq!(saved.repo().rev(), rev(2), "the head is not the cut");
349 drop(saved);
350
351 let reopened = Store::open(&to, fixture::clock()).expect("the container reopens");
352 assert!(
353 reopened.read_only_reason().is_none(),
354 "the saved container did not verify: {:?}",
355 reopened.read_only_reason(),
356 );
357 assert_eq!(
358 reopened.document().clone(),
359 at_two,
360 "the saved container is not the document that rev held",
361 );
362 assert_eq!(
363 reopened.projection(),
364 crate::store::projection::Freshness::Fresh,
365 "the projection beside the new log does not stamp its own head",
366 );
367 }
368
369 #[test]
373 fn a_tag_inside_the_cut_survives_and_one_past_it_does_not() {
374 let dir = fixture::dir("prefix-tags");
375 let from = dir.join("source.bwx");
376 let held = source(&from);
377 assert_eq!(held.tags().len(), 2, "precondition: both revs are named");
378 drop(held);
379
380 let to = dir.join("through-2.bwx");
381 let saved = save_through(
382 &from,
383 rev(2),
384 &to,
385 fixture::clock(),
386 &Identity::new("grace"),
387 )
388 .expect("the prefix saves");
389 assert_eq!(saved.tags().of(rev(1)), ["the start"]);
390 assert_eq!(
391 saved.tags().of(rev(3)),
392 Vec::<String>::new(),
393 "a rev past the cut came over with its name",
394 );
395 }
396
397 #[test]
401 fn a_saved_prefix_takes_the_next_edit_normally() {
402 let dir = fixture::dir("prefix-authoring");
403 let from = dir.join("source.bwx");
404 drop(source(&from));
405
406 let to = dir.join("through-1.bwx");
407 let mut saved = save_through(
408 &from,
409 rev(1),
410 &to,
411 fixture::clock(),
412 &Identity::new("grace"),
413 )
414 .expect("the prefix");
415 let next = saved
416 .submit_edit(fixture::edits(3)[1].clone(), &Identity::new("grace"))
417 .expect("the edit lands on the saved prefix");
418 assert_eq!(
419 next,
420 rev(2),
421 "the saved container did not author from its own head"
422 );
423 drop(saved);
424
425 let reopened = Store::open(&to, fixture::clock()).expect("the container reopens");
426 assert_eq!(reopened.repo().rev(), rev(2));
427 assert!(
428 reopened.read_only_reason().is_none(),
429 "the edit broke the chain"
430 );
431 }
432
433 #[test]
438 fn a_name_given_later_to_a_rev_inside_the_cut_is_written_again() {
439 let dir = fixture::dir("prefix-late-tag");
440 let from = dir.join("source.bwx");
441 {
442 let mut store = source(&from);
443 store
444 .tag(
445 rev(2),
446 "worth keeping",
447 Tagging::Added,
448 &Identity::new("ada"),
449 )
450 .expect("the late tag");
451 }
452
453 let to = dir.join("through-2.bwx");
454 let saved = save_through(
455 &from,
456 rev(2),
457 &to,
458 fixture::clock(),
459 &Identity::new("grace"),
460 )
461 .expect("the prefix saves");
462 assert_eq!(
463 saved.tags().of(rev(2)),
464 ["worth keeping"],
465 "a name given after the cut's own rev was left behind with it",
466 );
467 assert_eq!(saved.repo().rev(), rev(2), "a re-written tag spent a rev");
468 drop(saved);
469
470 let reopened = Store::open(&to, fixture::clock()).expect("the container reopens");
471 assert!(
472 reopened.read_only_reason().is_none(),
473 "the re-written tag broke the chain: {:?}",
474 reopened.read_only_reason(),
475 );
476 assert_eq!(reopened.tags().of(rev(2)), ["worth keeping"]);
477 }
478
479 #[test]
482 fn a_name_the_source_took_back_does_not_come_over() {
483 let dir = fixture::dir("prefix-untag");
484 let from = dir.join("source.bwx");
485 {
486 let mut store = source(&from);
487 store
488 .tag(rev(1), "the start", Tagging::Removed, &Identity::new("ada"))
489 .expect("the untag");
490 }
491
492 let to = dir.join("through-2.bwx");
493 let saved = save_through(
494 &from,
495 rev(2),
496 &to,
497 fixture::clock(),
498 &Identity::new("grace"),
499 )
500 .expect("the prefix saves");
501 assert_eq!(
502 saved.tags().of(rev(1)),
503 Vec::<String>::new(),
504 "the saved container kept a name the source had dropped",
505 );
506 }
507
508 #[test]
513 fn the_artwork_the_prefix_names_comes_with_it() {
514 let dir = fixture::dir("prefix-assets");
515 let from = dir.join("source.bwx");
516 let asset = {
517 let mut store = Store::create(&from, fixture::clock()).expect("the container");
518 let author = Identity::new("ada");
519 store
520 .submit_edit(fixture::edits(1)[0].clone(), &author)
521 .expect("the block lands");
522 let asset = fixture::svg(1);
523 store
524 .submit_edit(
525 fixture::commit("Added an icon", fixture::icon(1, &asset)),
526 &author,
527 )
528 .expect("the icon lands");
529 asset
530 };
531 assert_eq!(
532 std::fs::read_dir(from.join(ASSETS))
533 .expect("the source assets")
534 .count(),
535 1,
536 "precondition: the payload was extracted beside the source log",
537 );
538
539 let to = dir.join("through-2.bwx");
540 let saved = save_through(
541 &from,
542 rev(2),
543 &to,
544 fixture::clock(),
545 &Identity::new("grace"),
546 )
547 .expect("the prefix saves");
548 assert!(
549 saved.read_only_reason().is_none(),
550 "the saved container could not honour its own artwork: {:?}",
551 saved.read_only_reason(),
552 );
553 drop(saved);
554 let carried: Vec<Vec<u8>> = std::fs::read_dir(to.join(ASSETS))
555 .expect("the saved assets")
556 .map(|entry| std::fs::read(entry.expect("an entry").path()).expect("the payload"))
557 .collect();
558 assert_eq!(carried, vec![asset.bytes().to_vec()]);
559 }
560
561 #[test]
564 fn a_rev_the_log_does_not_hold_is_refused() {
565 let dir = fixture::dir("prefix-no-such-rev");
566 let from = dir.join("source.bwx");
567 drop(source(&from));
568
569 let to = dir.join("through-9.bwx");
570 assert!(matches!(
571 save_through(
572 &from,
573 rev(9),
574 &to,
575 fixture::clock(),
576 &Identity::new("grace")
577 ),
578 Err(PrefixFailure::NoSuchRev(_)),
579 ));
580 assert!(!to.exists(), "a refused save left a container behind");
581 }
582
583 #[test]
586 fn cutting_at_the_head_copies_every_line() {
587 let dir = fixture::dir("prefix-whole");
588 let from = dir.join("source.bwx");
589 drop(source(&from));
590
591 let to = dir.join("whole.bwx");
592 let saved = save_through(
593 &from,
594 rev(3),
595 &to,
596 fixture::clock(),
597 &Identity::new("grace"),
598 )
599 .expect("the whole log");
600 assert_eq!(saved.repo().rev(), rev(3));
601 assert_eq!(saved.tags().len(), 2, "a whole copy dropped a name");
602 drop(saved);
603 assert_eq!(
604 std::fs::read(from.join(MANIFEST)).expect("the source manifest"),
605 std::fs::read(to.join(MANIFEST)).expect("the copy"),
606 "a whole copy is not the same bytes",
607 );
608 }
609}