Oregami
Repositories/oxedyne/ore

oxedyne/ore/cli/src/arrived.rs

12.2 KiB, 40 runs

created by r2848102244:115, 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//! What the last sync delivered.
2//!
3//! A sync converges two histories and says how many operations crossed. What it
4//! does not say is *what* crossed, and the self-hosting trial's reviewer wanted
5//! exactly that: after a sync, which operations arrived, which files they
6//! touched, who wrote them, and what the arrival flagged
7//! (`self_hosting_trial.md` §3 item 3). `ore log --arrived` is that answer.
8//!
9//! # The record is two frontiers
10//!
11//! A sync writes down where the log stood before it and where it stood after,
12//! and the delta is recomputed from those whenever it is asked for. Keeping the
13//! frontiers rather than the list of operations is what makes the answer survive
14//! everything that happens next: work done since the sync is causally after the
15//! second frontier, so it is not mistaken for something that arrived, and a
16//! frontier is small however large the arrival was.
17//!
18//! It is this replica's own bookkeeping, in the manner of `.ore/snap/last` and
19//! `.ore/batches`: nothing in it is history, nothing in it syncs, and deleting
20//! `.ore/arrived` costs the answer to this one question until the next sync.
21//!
22//! # Only the last one
23//!
24//! One record, overwritten by each sync, and the output says which sync it is
25//! counting. Keeping every sync's delta would be a second log of a kind the
26//! operation graph already holds better: what a reviewer does after a sync is
27//! read what that sync brought, and what an older sync brought is `ore log`.
28//!
29//! Both ends of a sync get a record, because both ends absorb. The one the
30//! command was run from writes its own; the other is written into by the same
31//! command, exactly as the pending marker and the batch record there are.
32
33use crate::capture;
34use crate::keys::{
35 mark_of,
36 Prov,
37};
38use crate::place::Where;
39use crate::repo::{
40 ids_from_dat,
41 ids_to_dat,
42 number,
43 Repo,
44 ORE_DIR,
45};
46use crate::reviewed::{
47 ident,
48 Reviewed,
49};
50use crate::tree;
51use crate::verbs;
52
53use oxedyne_fe2o3_core::prelude::*;
54use oxedyne_fe2o3_jdat::prelude::*;
55use oxedyne_fe2o3_ore::log::OpLog;
56use oxedyne_fe2o3_ore::id::{
57 OpId,
58 ReplicaId,
59};
60use oxedyne_fe2o3_ore::op::{
61 Op,
62 Record,
63};
64use oxedyne_fe2o3_ore::seq::render::Flag;
65
66use std::collections::{
67 BTreeMap,
68 BTreeSet,
69};
70use std::fs;
71use std::path::{
72 Path,
73 PathBuf,
74};
75
76
77/// Name of the record, within [`ORE_DIR`].
78pub const ARRIVED_FILE: &str = "arrived";
79
80
81/// Where the record lives.
82pub fn path(root: &Path) -> PathBuf {
83 root.join(ORE_DIR).join(ARRIVED_FILE)
84}
85
86
87/// What one sync did to this repository: who it was with, and the frontier
88/// either side of it.
89#[derive(Clone, Debug)]
90pub struct Arrival {
91 /// The repository or relay the operations came from, for the message.
92 pub from: String,
93 /// The frontier before the exchange.
94 pub before: Vec<OpId>,
95 /// The frontier after it.
96 pub after: Vec<OpId>,
97 /// How many syncs this repository has recorded, this one included, so that
98 /// the output can say which one it is describing.
99 pub count: u64,
100}
101
102impl Arrival {
103
104 /// Serialises the record to a [`Dat`].
105 pub fn to_dat(&self) -> Dat {
106 let mut map = DaticleMap::new();
107 map.insert(Dat::Str(fmt!("from")), Dat::Str(self.from.clone()));
108 map.insert(Dat::Str(fmt!("before")), ids_to_dat(&self.before));
109 map.insert(Dat::Str(fmt!("after")), ids_to_dat(&self.after));
110 map.insert(Dat::Str(fmt!("count")), Dat::U64(self.count));
111 Dat::Map(map)
112 }
113
114 /// Reconstructs the record from a [`Dat`].
115 pub fn from_dat(dat: &Dat)
116 -> Outcome<Self>
117 {
118 let map = match dat {
119 Dat::Map(m) => m,
120 other => return Err(err!(
121 "An arrival record expects a map, got {:?}.", other;
122 Decode, Input, Mismatch)),
123 };
124 let from = match map.get(&Dat::Str(fmt!("from"))) {
125 Some(Dat::Str(s)) => s.clone(),
126 other => return Err(err!(
127 "An arrival record's origin expects a string, got {:?}.", other;
128 Decode, Input, Mismatch)),
129 };
130 let count = match map.get(&Dat::Str(fmt!("count"))) {
131 Some(dat) => res!(number(dat)),
132 None => 1,
133 };
134 Ok(Self {
135 from,
136 before: res!(ids_from_dat(
137 map.get(&Dat::Str(fmt!("before"))), "an arrival record's first frontier")),
138 after: res!(ids_from_dat(
139 map.get(&Dat::Str(fmt!("after"))), "an arrival record's second frontier")),
140 count,
141 })
142 }
143}
144
145
146/// Reads the record, which a repository that has never synced does not have.
147pub fn read(root: &Path)
148 -> Outcome<Option<Arrival>>
149{
150 let at = path(root);
151 if !at.is_file() {
152 return Ok(None);
153 }
154 let text = match fs::read_to_string(&at) {
155 Ok(t) => t,
156 Err(e) => return Err(err!(e,
157 "The arrival record {:?} could not be read.", at;
158 IO, File, Read)),
159 };
160 let dat = match Dat::decode_string(text) {
161 Ok(d) => d,
162 Err(e) => return Err(err!(e,
163 "The arrival record {:?} is not readable JDAT. Deleting it costs what the \
164 last sync brought and nothing else.", at;
165 Decode, Input)),
166 };
167 Ok(Some(res!(Arrival::from_dat(&dat))))
168}
169
170/// Writes the record of a sync, counting it.
171///
172/// The count is read from whatever is there, so a repository whose record has
173/// been deleted starts counting again, and says a lower number rather than a
174/// wrong one.
175pub fn record(root: &Path, from: &str, before: Vec<OpId>, after: Vec<OpId>)
176 -> Outcome<()>
177{
178 let count = match res!(read(root)) {
179 Some(last) => last.count + 1,
180 None => 1,
181 };
182 let arrival = Arrival { from: fmt!("{}", from), before, after, count };
183 let text = res!(arrival.to_dat().jdat_to_lines(" "));
184 let at = path(root);
185 match fs::write(&at, fmt!("{}\n", text)) {
186 Ok(()) => Ok(()),
187 Err(e) => Err(err!(e,
188 "The arrival record {:?} could not be written.", at;
189 IO, File, Write)),
190 }
191}
192
193
194/// `ore log --arrived` -- what the last sync delivered.
195pub fn arrived(repo: &mut Repo)
196 -> Outcome<()>
197{
198 let what = res!(capture::capture(repo));
199 verbs::report(&what);
200 println!();
201 let last = match res!(read(&repo.root)) {
202 Some(a) => a,
203 None => {
204 println!("no sync has brought anything into this repository; there is nothing \
205 to have arrived");
206 return Ok(());
207 },
208 };
209 println!("the last sync was with {}", last.from);
210 if last.count > 1 {
211 println!("it is the {} this repository has recorded, and only the last is kept",
212 ordinal(last.count));
213 }
214 return describe(&repo.log, &repo.root, &repo.prov, repo.cfg.replica, &last.before,
215 &last.after, "it brought");
216}
217
218/// Says what one log holds at `after` that it did not hold at `before`.
219///
220/// The two frontiers are read against the same log, so this answers for an
221/// exchange that has happened and equally for one that has only been rehearsed
222/// into a copy: what arrived is what the second state holds and the first does
223/// not. A sync authors nothing of its own, and anything written since is
224/// causally after the second frontier and so is in neither set.
225///
226/// `did` is how the sentence about the count begins, since the same description
227/// serves a sync that has happened and one that would.
228pub fn describe(
229 log: &OpLog,
230 root: &Path,
231 prov: &BTreeMap<OpId, Prov>,
232 mine: ReplicaId,
233 before: &[OpId],
234 after: &[OpId],
235 did: &str,
236)
237 -> Outcome<()>
238{
239 let was: BTreeSet<OpId> = res!(tree::ancestry(log, before))
240 .iter().map(|rec| rec.id()).collect();
241 let now = res!(tree::ancestry(log, after));
242 let new: Vec<&Record> = now.iter()
243 .filter(|rec| !was.contains(&rec.id()))
244 .copied()
245 .collect();
246
247 if new.is_empty() {
248 println!("{} nothing: both repositories already held the same history", did);
249 return Ok(());
250 }
251 println!("{} {} operation{}",
252 did, new.len(), if new.len() == 1 { "" } else { "s" });
253
254 // What the operations were, counted by kind rather than listed: an arrival of
255 // a thousand operations is not worth a thousand lines, and the files and the
256 // authors below are what a reviewer acts on.
257 let mut kinds: BTreeMap<(u8, &'static str, &'static str), usize> = BTreeMap::new();
258 let mut who: BTreeMap<ReplicaId, usize> = BTreeMap::new();
259 for rec in &new {
260 *kinds.entry(kind_of(&rec.op)).or_default() += 1;
261 *who.entry(rec.id().replica).or_default() += 1;
262 }
263 let said: Vec<String> = kinds.iter()
264 .map(|((_, one, many), n)| fmt!("{} {}", n, if *n == 1 { one } else { many }))
265 .collect();
266 println!(" what {}", said.join(", "));
267
268 // The state the sync left, which is what says where those operations landed.
269 let tree = res!(tree::at(log, after));
270 let mut files: BTreeSet<OpId> = BTreeSet::new();
271 for rec in &new {
272 // Which file an operation reached is one question, asked once. It used to
273 // be asked three ways here and two ways in the forge, which is how the two
274 // came to differ.
275 if let Some(f) = tree::file_reached(&tree.repo, rec) {
276 files.insert(f);
277 }
278 }
279 if files.is_empty() {
280 println!(" files none: nothing that arrived reached a file");
281 } else {
282 let named: Vec<String> = files.iter()
283 .map(|f| verbs::name_of(&tree.repo, *f))
284 .collect();
285 println!(" files {}", named.join(", "));
286 }
287
288 let mut authors: Vec<String> = Vec::new();
289 for (replica, n) in &who {
290 authors.push(fmt!("{} wrote {}", replica, n));
291 }
292 println!(" authors {}", authors.join(", "));
293 println!(" marks {}", match new.iter()
294 .filter_map(|rec| match &rec.op {
295 Op::Mark { name, .. } => Some(fmt!("{:?} {}", name, mark_of(prov, rec.id()))),
296 _ => None,
297 })
298 .collect::<Vec<String>>()
299 {
300 v if v.is_empty() => fmt!("none"),
301 v => v.join(", "),
302 });
303
304 // What the arrival flagged, which is the difference between what the renderer
305 // noticed before it and after it. A flag is a function of the operation set,
306 // so this is a set difference and not a record of anything.
307 let stood = res!(tree::at(log, before));
308 let held: BTreeSet<String> = stood.repo.flags().iter().map(ident).collect();
309 let raised: Vec<&Flag> = tree.repo.flags().iter()
310 .filter(|f| !held.contains(&ident(f)))
311 .collect();
312 if raised.is_empty() {
313 println!(" flags none: the arrival raised nothing the renderer had not \
314 already noticed");
315 return Ok(());
316 }
317 println!(" flags {} raised by the arrival",
318 raised.len());
319 let placed = Where::of(&tree);
320 let seen = res!(Reviewed::read(root));
321 for flag in &raised {
322 println!(" {}{}", verbs::describe(flag, &tree.repo).line(),
323 if seen.holds(flag) { " (reviewed)" } else { "" });
324 println!(" {}", placed.of_flag(flag, log));
325 }
326
327 // What it costs this replica, said separately from what it raised. A reader
328 // deciding whether to take somebody's work wants the price before the
329 // inventory, and the price is the edits of their own that the arbitration
330 // buries: written here, in the log, and not in the file afterwards. No system
331 // that stores states can answer this before the merge, because the answer is
332 // a property of the operations and not of the two versions.
333 let buried: Vec<&&Flag> = raised.iter()
334 .filter(|f| match f {
335 Flag::Yielded { op, .. } => op.replica == mine,
336 _ => false,
337 })
338 .collect();
339 if buried.is_empty() {
340 return Ok(());
341 }
342 println!(" buried {} edit{} of yours: in the log, not in the file",
343 buried.len(), if buried.len() == 1 { "" } else { "s" });
344 for flag in buried {
345 println!(" {}", placed.of_flag(flag, log));
346 }
347 Ok(())
348}
349
350/// Names an operation for a count, one and many, in the words the rest of the
351/// tool uses, with the rank the counts are listed in.
352///
353/// The rank is what a reader wants first rather than what the vocabulary lists
354/// first: a file appearing or going is the largest thing that can have happened,
355/// then what was done to the contents of one, and a mark is a label on the lot.
356fn kind_of(op: &Op) -> (u8, &'static str, &'static str) {
357 match op {
358 Op::FileCreate { .. } => (0, "file created", "files created"),
359 Op::FileDelete { .. } => (1, "file deleted", "files deleted"),
360 Op::FileRename { .. } => (2, "rename", "renames"),
361 Op::FileMode { .. } => (3, "mode change", "mode changes"),
362 Op::Splice { .. } => (4, "edit", "edits"),
363 Op::Move { .. } => (5, "move", "moves"),
364 Op::Note { .. } => (6, "note", "notes"),
365 Op::Mark { .. } => (7, "mark", "marks"),
366 Op::Proposal { .. } => (8, "proposal", "proposals"),
367 Op::Said { .. } => (9, "remark", "remarks"),
368 Op::Settled { .. } => (10, "settlement", "settlements"),
369 Op::Reverts { .. } => (11, "revert", "reverts"),
370 }
371}
372
373/// Writes a small ordinal in words, since the sentence it goes in is a sentence.
374fn ordinal(n: u64) -> String {
375 match n {
376 2 => fmt!("second sync"),
377 3 => fmt!("third sync"),
378 4 => fmt!("fourth sync"),
379 5 => fmt!("fifth sync"),
380 _ => fmt!("{}th sync", n),
381 }
382}