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 | |
| 34 | use crate::repo::{ |
| 35 | Repo, |
| 36 | ORE_DIR, |
| 37 | }; |
| 38 | use crate::tree::Tree; |
| 39 | |
| 40 | use oxedyne_fe2o3_core::prelude::*; |
| 41 | use oxedyne_fe2o3_ore::id::OpId; |
| 42 | use oxedyne_fe2o3_ore::op::Op; |
| 43 | use oxedyne_fe2o3_ore::seq::render::Flag; |
| 44 | |
| 45 | use std::collections::BTreeMap; |
| 46 | use std::fs; |
| 47 | use std::path::{ |
| 48 | Path, |
| 49 | PathBuf, |
| 50 | }; |
| 51 | |
| 52 | |
| 53 | /// Name of the collision directory, within [`ORE_DIR`]. |
| 54 | pub const COLLISION_DIR: &str = "collisions"; |
| 55 | |
| 56 | |
| 57 | /// Where the spill lives. |
| 58 | pub 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. |
| 66 | pub 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. |
| 78 | pub 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. |
| 88 | pub 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 | } |