Oregami
Repositories/oxedyne/ore

oxedyne/ore/cli/src/reviewed.rs

8.5 KiB, 1 run

created by r2848102244:128, 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//! Which flags this working copy has already looked at.
2//!
3//! A flag is a function of the operation set, so it is raised for ever: a
4//! function two people rewrote at once carries its `overlap` after the region has
5//! been repaired, because the concurrent naming it records remains true of the
6//! history. The self-hosting trial found the consequence -- by the fifth round
7//! the flag list had been identical for three syncs, and a reviewer was re-reading
8//! a growing list to find the one new entry (`self_hosting_trial.md` §2.5).
9//!
10//! This is the answer: a flag can be marked reviewed, and `ore flags --new` shows
11//! only the rest.
12//!
13//! # It is this replica's bookkeeping and nothing else
14//!
15//! `.ore/reviewed` is the tool's memory in the manner of `.ore/batches`: nothing
16//! in it is history, nothing in it syncs, and no other replica has an opinion
17//! about it. **Deleting the file makes every flag new again**, which costs
18//! nothing but a second reading. Reviewing is a person's act, and a person's acts
19//! belong to the tool; causality belongs to the graph.
20//!
21//! # What a flag is called
22//!
23//! Its kind and the operations it names, never its place in a list. A list
24//! position is not an identity -- one arriving operation reorders the list and
25//! every mark would move to the wrong flag -- and a rendered coordinate is not
26//! one either, since a later edit moves it. What does not move is which
27//! operations raced, so that is the name: `overlap:r1.14,r2.14`, with the colon
28//! inside an identity written as a full stop, the convention `.ore/collisions/`
29//! already uses.
30//!
31//! A re-render may retire a flag: an arbitration that changes takes its yield
32//! flag with it. A reviewed name that no longer occurs is dropped the next time
33//! the record is written, which is the next `--reviewed`, so the file cannot
34//! outgrow the flag list. Nothing is written by merely reading the flags, since
35//! a verb that prints a list should not have to write a file to do it.
36//!
37//! # The one-writer rule
38//!
39//! This, with the spill of `.ore/collisions/`, is the command line tool's half of
40//! what the trial said the one-writer-per-repository rule costs to retire
41//! unconditionally: file-relative flag coordinates, a since-last-sync delta, and
42//! somewhere to put a flag down (`self_hosting_trial.md` §3 item 3). The other
43//! half is the forge's -- the side-by-side view of each author's version of a
44//! contended region -- and it is not here.
45
46use crate::repo::{
47 Repo,
48 ORE_DIR,
49};
50
51use oxedyne_fe2o3_core::prelude::*;
52use oxedyne_fe2o3_jdat::prelude::*;
53use oxedyne_fe2o3_ore::id::OpId;
54use oxedyne_fe2o3_ore::seq::render::Flag;
55
56use std::collections::BTreeSet;
57use std::fs;
58use std::path::{
59 Path,
60 PathBuf,
61};
62
63
64/// Name of the reviewed record, within [`ORE_DIR`].
65pub const REVIEWED_FILE: &str = "reviewed";
66
67/// What `ore flags --reviewed` takes to mean every flag there is.
68pub const ALL: &str = "all";
69
70
71/// Where the record lives.
72pub fn path(root: &Path) -> PathBuf {
73 root.join(ORE_DIR).join(REVIEWED_FILE)
74}
75
76/// Writes an operation's identity the way a filename and a record can hold it.
77pub fn plain(op: &OpId) -> String {
78 fmt!("{}", op).replace(':', ".")
79}
80
81/// Returns the name a flag is remembered by: its kind, and the operations it
82/// names.
83///
84/// Where a kind can raise two flags about one operation -- an origin demoted at
85/// two offsets, say -- the offset is part of the name, because those are two
86/// things to look at and not one.
87pub fn ident(flag: &Flag) -> String {
88 match flag {
89 Flag::Torn { op, .. } => fmt!("torn:{}", plain(op)),
90 Flag::Demoted { op, sub, origin } => fmt!(
91 "demoted:{}+{}.{}", plain(op), sub, origin),
92 Flag::Dropped { op, sub, origin } => fmt!(
93 "dropped:{}+{}.{}", plain(op), sub, origin),
94 Flag::Overlap { ops, .. } => fmt!("overlap:{}",
95 ops.iter().map(plain).collect::<Vec<String>>().join(",")),
96 Flag::CrossedFile { op, sub, .. } => fmt!("crossed:{}+{}", plain(op), sub),
97 Flag::MovedIntoDeleted { op, file } => fmt!(
98 "buried:{},{}", plain(op), plain(file)),
99 Flag::Orphaned { op, sub } => fmt!("orphaned:{}+{}", plain(op), sub),
100 Flag::Confined { op, .. } => fmt!("confined:{}", plain(op)),
101 Flag::Won { op } => fmt!("won:{}", plain(op)),
102 Flag::Stranded { op, by } => fmt!("stranded:{},{}", plain(op), plain(by)),
103 Flag::SplicedIntoDeleted { op, del, .. } => fmt!(
104 "swallowed:{},{}", plain(op), plain(del)),
105 Flag::Yielded { op, to, .. } => fmt!("yielded:{},{}", plain(op), plain(to)),
106 }
107}
108
109/// Returns every operation a flag names, as text, which is how a person names
110/// one flag rather than all of them.
111pub fn names(flag: &Flag) -> Vec<String> {
112 let mut out: Vec<String> = Vec::new();
113 match flag {
114 Flag::Overlap { ops, .. } => out.extend(ops.iter().map(|id| fmt!("{}", id))),
115 Flag::Stranded { op, by } => out.extend([fmt!("{}", op), fmt!("{}", by)]),
116 Flag::SplicedIntoDeleted { op, del, .. } => out.extend(
117 [fmt!("{}", op), fmt!("{}", del)]),
118 Flag::Yielded { op, to, .. } => out.extend([fmt!("{}", op), fmt!("{}", to)]),
119 other => {
120 if let Some(op) = other.op() {
121 out.push(fmt!("{}", op));
122 }
123 },
124 }
125 out
126}
127
128
129/// The flags this working copy has been told it has seen.
130#[derive(Clone, Debug, Default)]
131pub struct Reviewed {
132 /// The names, as [`ident`] writes them.
133 seen: BTreeSet<String>,
134}
135
136impl Reviewed {
137
138 /// Reads the record, which a repository is not obliged to have.
139 ///
140 /// A file that will not decode is an error like any other: this tool wrote
141 /// it, it is small, and starting again silently would tell a reviewer that
142 /// work they had done was undone without saying so.
143 pub fn read(root: &Path)
144 -> Outcome<Self>
145 {
146 let at = path(root);
147 if !at.is_file() {
148 return Ok(Self::default());
149 }
150 let text = match fs::read_to_string(&at) {
151 Ok(t) => t,
152 Err(e) => return Err(err!(e,
153 "The reviewed record {:?} could not be read.", at;
154 IO, File, Read)),
155 };
156 let dat = match Dat::decode_string(text) {
157 Ok(d) => d,
158 Err(e) => return Err(err!(e,
159 "The reviewed record {:?} is not readable JDAT. Deleting it makes every \
160 flag new again, which is the whole of what it costs.", at;
161 Decode, Input)),
162 };
163 let listed = match &dat {
164 Dat::List(l) => l,
165 other => return Err(err!(
166 "The reviewed record {:?} expects a list, got {:?}.", at, other;
167 Decode, Input, Mismatch)),
168 };
169 let mut seen = BTreeSet::new();
170 for item in listed {
171 match item {
172 Dat::Str(s) => { seen.insert(s.clone()); },
173 other => return Err(err!(
174 "A reviewed flag expects a string, got {:?}.", other;
175 Decode, Input, Mismatch)),
176 }
177 }
178 Ok(Self { seen })
179 }
180
181 /// Reports whether a flag has been marked reviewed.
182 pub fn holds(&self, flag: &Flag) -> bool {
183 self.seen.contains(&ident(flag))
184 }
185
186 /// Marks every flag `which` names, and returns how many were newly marked.
187 ///
188 /// `which` is either [`ALL`] or an operation identifier as the flags print
189 /// one. Naming an operation marks every flag that names it, which is what a
190 /// person means: an edit that raised an overlap and a yield raised them about
191 /// one thing, and putting one of them down and not the other is not something
192 /// anybody wants.
193 pub fn mark(&mut self, flags: &[Flag], which: &str) -> usize {
194 let mut marked = 0usize;
195 for flag in flags {
196 if which != ALL && !names(flag).iter().any(|id| id == which) {
197 continue;
198 }
199 if self.seen.insert(ident(flag)) {
200 marked += 1;
201 }
202 }
203 marked
204 }
205
206 /// Returns how many of the flags given are marked reviewed.
207 pub fn count(&self, flags: &[Flag]) -> usize {
208 flags.iter().filter(|f| self.holds(f)).count()
209 }
210
211 /// Writes the record, keeping only what the flags given still raise.
212 ///
213 /// A flag that no longer occurs -- an arbitration that changed, a history that
214 /// grew past it -- is dropped rather than kept, so the record stays the size of
215 /// the flag list and a flag that returns returns new.
216 pub fn save(&self, repo: &Repo, flags: &[Flag])
217 -> Outcome<()>
218 {
219 let occurring: BTreeSet<String> = flags.iter().map(ident).collect();
220 let keep: Vec<Dat> = self.seen.iter()
221 .filter(|name| occurring.contains(*name))
222 .map(|name| Dat::Str(name.clone()))
223 .collect();
224 let at = path(&repo.root);
225 if keep.is_empty() {
226 if !at.is_file() {
227 return Ok(());
228 }
229 return match fs::remove_file(&at) {
230 Ok(()) => Ok(()),
231 Err(e) => Err(err!(e,
232 "The reviewed record {:?} could not be removed.", at;
233 IO, File, Write)),
234 };
235 }
236 let text = res!(Dat::List(keep).jdat_to_lines(" "));
237 match fs::write(&at, fmt!("{}\n", text)) {
238 Ok(()) => Ok(()),
239 Err(e) => Err(err!(e,
240 "The reviewed record {:?} could not be written.", at;
241 IO, File, Write)),
242 }
243 }
244}