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 | |
| 33 | use crate::capture; |
| 34 | use crate::keys::{ |
| 35 | mark_of, |
| 36 | Prov, |
| 37 | }; |
| 38 | use crate::place::Where; |
| 39 | use crate::repo::{ |
| 40 | ids_from_dat, |
| 41 | ids_to_dat, |
| 42 | number, |
| 43 | Repo, |
| 44 | ORE_DIR, |
| 45 | }; |
| 46 | use crate::reviewed::{ |
| 47 | ident, |
| 48 | Reviewed, |
| 49 | }; |
| 50 | use crate::tree; |
| 51 | use crate::verbs; |
| 52 | |
| 53 | use oxedyne_fe2o3_core::prelude::*; |
| 54 | use oxedyne_fe2o3_jdat::prelude::*; |
| 55 | use oxedyne_fe2o3_ore::log::OpLog; |
| 56 | use oxedyne_fe2o3_ore::id::{ |
| 57 | OpId, |
| 58 | ReplicaId, |
| 59 | }; |
| 60 | use oxedyne_fe2o3_ore::op::{ |
| 61 | Op, |
| 62 | Record, |
| 63 | }; |
| 64 | use oxedyne_fe2o3_ore::seq::render::Flag; |
| 65 | |
| 66 | use std::collections::{ |
| 67 | BTreeMap, |
| 68 | BTreeSet, |
| 69 | }; |
| 70 | use std::fs; |
| 71 | use std::path::{ |
| 72 | Path, |
| 73 | PathBuf, |
| 74 | }; |
| 75 | |
| 76 | |
| 77 | /// Name of the record, within [`ORE_DIR`]. |
| 78 | pub const ARRIVED_FILE: &str = "arrived"; |
| 79 | |
| 80 | |
| 81 | /// Where the record lives. |
| 82 | pub 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)] |
| 90 | pub 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 | |
| 102 | impl 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. |
| 147 | pub 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. |
| 175 | pub 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. |
| 195 | pub 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. |
| 228 | pub 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. |
| 356 | fn 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. |
| 374 | fn 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 | } |