Oregami
Repositories/oxedyne/ore

oxedyne/ore/cli/src/collisions.rs

5.1 KiB, 3 runs

created by r2848102244:117, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! The losing version of a contended region, spilled beside the working copy.
2//!
3//! When two people rewrite one region at once the renderer arbitrates: the
4//! group's op-order maximum prevails and every member concurrent with it yields,
5//! its insertion buried whole. The working file then holds one version, and
6//! [`oxedyne_fe2o3_ore::seq::render::Flag::Yielded`] says whose it is not. This
7//! writes the other version out, one file per yielded operation, so that a
8//! reviewer with no forge at all can `diff` the two.
9//!
10//! # It is derived data
11//!
12//! Nothing here is history and nothing here syncs. `.ore/collisions/` is a
13//! function of the render, it is rewritten from scratch on every verb that
14//! renders, and deleting the whole directory costs nothing: the next render puts
15//! back exactly what was there. A file for an operation that no longer yields is
16//! removed rather than left, because a stale spill is worse than no spill --
17//! it names a collision that the history no longer holds.
18//!
19//! It is safe from self-capture because `.ore` is one of the two names the
20//! working-copy scan skips outright, so nothing written here can come back as an
21//! authored edit.
22//!
23//! # What a file holds
24//!
25//! The buried bytes themselves: the insertion of the yielded splice, exactly as
26//! its author wrote it, and nothing else. Not the file that author would have
27//! rendered -- that is a counterfactual render and the review surface's job --
28//! and not a diff, which would need a second version to be a diff against.
29//!
30//! The name is the operation's identity with its colon written as a full stop,
31//! since a colon is not a filename on every filesystem this tool may meet;
32//! `r5:12` in a flag is `r5.12` here.
33
34use crate::repo::{
35 Repo,
36 ORE_DIR,
37};
38use crate::tree::Tree;
39
40use oxedyne_fe2o3_core::prelude::*;
41use oxedyne_fe2o3_ore::id::OpId;
42use oxedyne_fe2o3_ore::op::Op;
43use oxedyne_fe2o3_ore::seq::render::Flag;
44
45use std::collections::BTreeMap;
46use std::fs;
47use std::path::{
48 Path,
49 PathBuf,
50};
51
52
53/// Name of the collision directory, within [`ORE_DIR`].
54pub const COLLISION_DIR: &str = "collisions";
55
56
57/// Where the spill lives.
58pub fn dir(root: &Path) -> PathBuf {
59 root.join(ORE_DIR).join(COLLISION_DIR)
60}
61
62/// Says where the buried versions are, where there are any.
63///
64/// One sentence, and it says the directory is derived, because a reader who finds
65/// files in `.ore` and does not know that will treat them as something to keep.
66pub fn report(spilled: &[OpId]) {
67 if spilled.is_empty() {
68 return;
69 }
70 println!("the version{} that yielded {} in {}/{}, which is derived and may be \
71 deleted",
72 if spilled.len() == 1 { "" } else { "s" },
73 if spilled.len() == 1 { "is" } else { "are" },
74 ORE_DIR, COLLISION_DIR);
75}
76
77/// The filename an operation's buried version is written under.
78pub fn name_of(op: &OpId) -> String {
79 fmt!("{}", op).replace(':', ".")
80}
81
82/// Writes every buried insertion the render holds, removes anything left over
83/// from a previous one, and returns the operations spilled in op order.
84///
85/// A yielded operation that inserted nothing has nothing to spill: a pure
86/// deletion that yields simply does not bury, so there is no other version of the
87/// region to show.
88pub fn spill(repo: &Repo, tree: &Tree)
89 -> Outcome<Vec<OpId>>
90{
91 let mut want: BTreeMap<OpId, Vec<u8>> = BTreeMap::new();
92 for flag in tree.repo.flags() {
93 let op = match flag {
94 Flag::Yielded { op, .. } => *op,
95 _ => continue,
96 };
97 let rec = match repo.log.get(&op) {
98 Some(r) => r,
99 None => continue,
100 };
101 if let Op::Splice { insert, .. } = &rec.op {
102 if !insert.is_empty() {
103 want.insert(op, insert.to_vec());
104 }
105 }
106 }
107 let dir = dir(&repo.root);
108 if want.is_empty() {
109 // Nothing is contended, so nothing may be left saying otherwise.
110 if dir.is_dir() {
111 match fs::remove_dir_all(&dir) {
112 Ok(()) => (),
113 Err(e) => return Err(err!(e,
114 "The collision directory {:?} could not be cleared.", dir;
115 IO, File, Write)),
116 }
117 }
118 return Ok(Vec::new());
119 }
120 match fs::create_dir_all(&dir) {
121 Ok(()) => (),
122 Err(e) => return Err(err!(e,
123 "The collision directory {:?} could not be created.", dir;
124 IO, File, Write)),
125 }
126 let keep: Vec<String> = want.keys().map(name_of).collect();
127 for (op, bytes) in &want {
128 let at = dir.join(name_of(op));
129 match fs::write(&at, bytes) {
130 Ok(()) => (),
131 Err(e) => return Err(err!(e,
132 "The buried version of {} could not be written to {:?}.", op, at;
133 IO, File, Write)),
134 }
135 }
136 // Whatever a previous render left that this one does not claim.
137 let listed = match fs::read_dir(&dir) {
138 Ok(l) => l,
139 Err(e) => return Err(err!(e,
140 "The collision directory {:?} could not be read.", dir;
141 IO, File, Read)),
142 };
143 for entry in listed {
144 let entry = match entry {
145 Ok(e) => e,
146 Err(e) => return Err(err!(e,
147 "The collision directory {:?} could not be walked.", dir;
148 IO, File, Read)),
149 };
150 let name = entry.file_name().to_string_lossy().into_owned();
151 if keep.contains(&name) {
152 continue;
153 }
154 match fs::remove_file(entry.path()) {
155 Ok(()) => (),
156 Err(e) => return Err(err!(e,
157 "The stale spill {:?} could not be removed.", entry.path();
158 IO, File, Write)),
159 }
160 }
161 Ok(want.into_keys().collect())
162}