oxedyne/ore/cli/src/capture.rs
28.4 KiB, 61 runs
created by r2848102244:7, 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 | //! Reading the working copy, and turning what changed into operations. |
| 2 | //! |
| 3 | //! Every verb but `init` begins by capturing. Nothing is staged and nothing is |
| 4 | //! committed: the working copy is compared with what the operations say it |
| 5 | //! should be, and the difference is appended to the log as more operations. A |
| 6 | //! command therefore never finds itself looking at a state the history has not |
| 7 | //! recorded. |
| 8 | //! |
| 9 | //! # The difference is the engine's to find |
| 10 | //! |
| 11 | //! A changed file is put through [`oxedyne_fe2o3_ore::diff`], which returns the |
| 12 | //! ordered splices that turn the old bytes into the new. Every offset it |
| 13 | //! reports is an offset into the **old** bytes, and the old bytes are what the |
| 14 | //! render holds, so the whole list resolves against that one render: |
| 15 | //! [`Rendered::splice`] turns each positional splice into a content-anchored |
| 16 | //! operation, and the splices are far enough apart that none of them anchors on |
| 17 | //! content another removes. |
| 18 | //! |
| 19 | //! Each splice becomes an operation of its own, authored one after another, so |
| 20 | //! that two edits at opposite ends of a file are two small operations rather |
| 21 | //! than one covering everything between them. What that buys is storage -- the |
| 22 | //! bytes recorded are the bytes typed -- and attribution, since `ore who` then |
| 23 | //! names each region's own author rather than one operation for the lot. |
| 24 | //! |
| 25 | //! A rename is still provisional: version zero sees a file gone and another |
| 26 | //! arrived, and records a delete and a create. The vocabulary has |
| 27 | //! `Op::FileRename` and the importer uses it, because git says outright that a |
| 28 | //! rename happened; detecting one from the working copy is later work. |
| 29 | //! |
| 30 | //! # A move is recorded as a move, when it is unmistakably one |
| 31 | //! |
| 32 | //! An editor's block move reaches the filesystem as bytes gone from one place |
| 33 | //! and the same bytes arrived in another, and a diff can only say so as a |
| 34 | //! deletion and an insertion. Recorded that way, the engine's move machinery |
| 35 | //! never engages: a concurrent edit inside the block dies with the deletion |
| 36 | //! instead of following the block, and `ore who` attributes the whole block to |
| 37 | //! the mover. So the capture diffs every changed file first and authors second, |
| 38 | //! and in between it looks for a removed region whose bytes reappear, exactly |
| 39 | //! and in one piece, as an inserted region elsewhere -- in the same file or in |
| 40 | //! another -- and records the pair as one [`Op::Move`]. |
| 41 | //! |
| 42 | //! The rules are deliberately conservative, because a wrong move rewrites |
| 43 | //! intent and a missed one merely costs what the tool never had: |
| 44 | //! |
| 45 | //! - **Exact byte equality only**, over at least [`MIN_MOVE_LEN`] bytes of both |
| 46 | //! regions. A block moved *and* edited in the same capture therefore degrades |
| 47 | //! to the deletion and insertion it always was, on purpose: pairing "nearly |
| 48 | //! equal" regions would be guessing which bytes the author meant to keep. |
| 49 | //! - **Unambiguous pairing only.** If the same bytes vanish once and appear |
| 50 | //! twice, or vanish twice and appear once, nothing is paired and every site |
| 51 | //! is authored as the splice the diff found -- which is yesterday's correct |
| 52 | //! but unflagged behaviour, not a regression. |
| 53 | //! - **Whole regions only.** A removed region is paired as one block or not at |
| 54 | //! all; no attempt is made to find a moved fragment inside a larger edit. |
| 55 | //! - **The source is content identity.** The removed disk region is mapped back |
| 56 | //! through the render's runs ([`Rendered::span`]) to the [`ContentRange`]s |
| 57 | //! that name those bytes, however many runs the region spans. That is the |
| 58 | //! whole point of the exercise: the move relocates the bytes the history |
| 59 | //! already knows, so everything anchored to them -- concurrent edits, notes, |
| 60 | //! provenance -- travels with them. |
| 61 | //! |
| 62 | //! Only regions of files that already exist on both sides are considered: |
| 63 | //! content leaving a file that is being deleted, or landing in one being |
| 64 | //! created, is out of this pass's scope and stays a deletion or an insertion. |
| 65 | //! |
| 66 | //! [`ContentRange`]: oxedyne_fe2o3_ore::id::ContentRange |
| 67 | //! |
| 68 | //! # A path is bytes, and a file is not its path |
| 69 | //! |
| 70 | //! The scan reads names as the filesystem holds them, so a name that is not |
| 71 | //! UTF-8 is captured like any other. Which file a path stands for is decided by |
| 72 | //! [`crate::tree::Layout`] and not here, so that the file a clash sent to a |
| 73 | //! derived name is compared against the bytes under that name rather than |
| 74 | //! captured a second time as something new. |
| 75 | //! |
| 76 | //! # What is scanned |
| 77 | //! |
| 78 | //! Every regular file under the root, except the two directories the version |
| 79 | //! control systems keep their own business in -- `.ore` and `.git` -- and |
| 80 | //! anything an optional `.oreignore` at the root matches. |
| 81 | //! |
| 82 | //! **A dotfile is content.** A name beginning with a full stop is not the |
| 83 | //! tool's business, it is the project's: `.gitignore`, `.oreignore`, |
| 84 | //! `.github/workflows`, `.cargo/config.toml`, `.env.example` and every editor |
| 85 | //! and CI configuration a repository is expected to carry are all files a |
| 86 | //! version control system exists to version. Skipping them by their first |
| 87 | //! character would lose them silently -- the working copy would hold them, the |
| 88 | //! history would not, and nothing would say so -- and `ore back` would then |
| 89 | //! delete them, since materialising a state removes what the state does not |
| 90 | //! describe. So the skip is by name and not by shape, it names exactly `.ore` |
| 91 | //! and `.git`, and everything else is governed by the ignore rules like any |
| 92 | //! other path. A repository that does not want its dotfiles versioned writes a |
| 93 | //! rule saying so, which is a decision recorded in the repository rather than |
| 94 | //! one buried in the scanner. `.oreignore` is captured under the same rule as |
| 95 | //! everything else rather than as a special case, which is what it always |
| 96 | //! needed to be: rules that stayed on one machine would ignore different things |
| 97 | //! on each. |
| 98 | //! |
| 99 | //! The ignore file speaks git's glob shapes, compiled by |
| 100 | //! [`oxedyne_fe2o3_file::glob`]: `*` |
| 101 | //! within a component, `**` across them, `?`, character classes, a leading `/` |
| 102 | //! to anchor at the root, a trailing `/` for directories only, and `!` to |
| 103 | //! re-include, the last matching line winning. Paths and patterns alike are |
| 104 | //! bytes, so a rule applies to a name that is not UTF-8 exactly as it applies |
| 105 | //! to one that is. |
| 106 | //! |
| 107 | //! The rules govern only what enters the history. A file the history already |
| 108 | //! tracks is read from disk no matter what the rules say of its path, as git |
| 109 | //! reads a tracked file no matter what `.gitignore` says: edits to it are |
| 110 | //! captured and its deletion is a deletion. Anything else would let a new |
| 111 | //! ignore rule quietly freeze a tracked file while the working copy walked |
| 112 | //! away from the history. |
| 113 | |
| 114 | use crate::guard; |
| 115 | use crate::repo::{ |
| 116 | Repo, |
| 117 | GIT_DIR, |
| 118 | IGNORE_FILE, |
| 119 | ORE_DIR, |
| 120 | }; |
| 121 | use crate::stat::{ |
| 122 | Seen, |
| 123 | Stats, |
| 124 | }; |
| 125 | use crate::tree; |
| 126 | |
| 127 | use oxedyne_fe2o3_core::prelude::*; |
| 128 | use oxedyne_fe2o3_file::glob::IgnoreFile; |
| 129 | use oxedyne_fe2o3_ore::diff; |
| 130 | use oxedyne_fe2o3_ore::id::{ |
| 131 | Anchor, |
| 132 | OpId, |
| 133 | }; |
| 134 | use oxedyne_fe2o3_ore::op::Op; |
| 135 | use oxedyne_fe2o3_ore::seq::render::Rendered; |
| 136 | use oxedyne_fe2o3_text::secret; |
| 137 | |
| 138 | use std::collections::BTreeMap; |
| 139 | use std::fs; |
| 140 | use std::path::Path; |
| 141 | |
| 142 | |
| 143 | /// Fewest bytes a removed region and an inserted region must both hold before |
| 144 | /// the capture will pair them as a move. |
| 145 | /// |
| 146 | /// The threshold trades missed moves against false ones. It must sit above the |
| 147 | /// fragments that recur verbatim throughout source code -- a closing brace |
| 148 | /// line, a blank line, a common `use` line, none of which clears a few dozen |
| 149 | /// bytes -- and below the smallest block a person would call a move, and even |
| 150 | /// a three-line function comfortably exceeds 64 bytes. Coincidental equality |
| 151 | /// would need the same 64 bytes deleted in one place and typed anew in another |
| 152 | /// within a single capture, which is implausible for anything but repeated |
| 153 | /// boilerplate; where boilerplate does repeat, the unambiguity rule is the |
| 154 | /// second gate, since the same bytes then appear at more than one site and |
| 155 | /// nothing is paired. |
| 156 | pub const MIN_MOVE_LEN: usize = 64; |
| 157 | |
| 158 | |
| 159 | /// What the working copy scan is told to leave alone. |
| 160 | /// |
| 161 | /// The rules are read from an optional `.oreignore` at the root and carry git's |
| 162 | /// glob shapes; the compiling and the matching live upstream in |
| 163 | /// [`oxedyne_fe2o3_file::glob`]. What is decided here is only what the rules |
| 164 | /// govern: paths that are not yet in the history. A tracked path is never |
| 165 | /// asked, because [`scan`] reads tracked files regardless. |
| 166 | #[derive(Clone, Debug, Default)] |
| 167 | pub struct Ignore { |
| 168 | /// The compiled rules, in the order the file gives them. |
| 169 | rules: IgnoreFile, |
| 170 | } |
| 171 | |
| 172 | impl Ignore { |
| 173 | /// Reads the ignore file at the root of a working copy, if there is one. |
| 174 | /// |
| 175 | /// The file is read as bytes, not text, so a rule can name a path that is |
| 176 | /// not UTF-8. |
| 177 | pub fn read(root: &Path) |
| 178 | -> Outcome<Self> |
| 179 | { |
| 180 | let path = root.join(IGNORE_FILE); |
| 181 | if !path.is_file() { |
| 182 | return Ok(Self::default()); |
| 183 | } |
| 184 | let bytes = match fs::read(&path) { |
| 185 | Ok(b) => b, |
| 186 | Err(e) => return Err(err!(e, |
| 187 | "The ignore file {:?} could not be read.", path; |
| 188 | IO, File, Read)), |
| 189 | }; |
| 190 | Ok(Self { rules: IgnoreFile::parse(&bytes) }) |
| 191 | } |
| 192 | |
| 193 | /// Reports whether the rules keep a path out of the history, where `is_dir` |
| 194 | /// says what the path names. The last matching rule wins, so a `!` rule |
| 195 | /// later in the file re-includes what an earlier rule shut out. |
| 196 | pub fn ignores(&self, path: &[u8], is_dir: bool) -> bool { |
| 197 | self.rules.ignores(path, is_dir) |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | |
| 202 | /// Reads every regular file of the working copy, by path. |
| 203 | /// |
| 204 | /// The ignore rules govern what is new; `tracked` names the paths the history |
| 205 | /// already holds, and each of those that stands on disk as a regular file is |
| 206 | /// read no matter what the rules say. A tracked path absent from the result is |
| 207 | /// therefore genuinely gone. |
| 208 | pub fn scan<'a, I>(root: &Path, ignore: &Ignore, tracked: I) |
| 209 | -> Outcome<(BTreeMap<Vec<u8>, Vec<u8>>, BTreeMap<Vec<u8>, Seen>)> |
| 210 | where |
| 211 | I: IntoIterator<Item = &'a Vec<u8>>, |
| 212 | { |
| 213 | let mut out = BTreeMap::new(); |
| 214 | let mut saw = BTreeMap::new(); |
| 215 | res!(walk(root, &[], ignore, &mut |rel: &[u8], full: &Path| { |
| 216 | // STATTED BEFORE IT IS READ, and that order is the safe one. A write |
| 217 | // between the two leaves the new bytes in the history beside the old |
| 218 | // file's size and time, so the next command reads the file again. The |
| 219 | // other order would put the new time beside the old bytes, and the next |
| 220 | // command would believe it. |
| 221 | let seen = match Seen::of(full) { |
| 222 | Some(s) => s, |
| 223 | None => return Ok(()), |
| 224 | }; |
| 225 | let bytes = match fs::read(full) { |
| 226 | Ok(b) => b, |
| 227 | Err(e) => return Err(err!(e, |
| 228 | "The file {:?} could not be read.", full; |
| 229 | IO, File, Read)), |
| 230 | }; |
| 231 | saw.insert(rel.to_vec(), seen); |
| 232 | out.insert(rel.to_vec(), bytes); |
| 233 | Ok(()) |
| 234 | })); |
| 235 | for path in tracked { |
| 236 | if out.contains_key(path) { |
| 237 | continue; |
| 238 | } |
| 239 | let full = res!(tree::on_disk(root, path)); |
| 240 | // Not following a symbolic link: what a link points at is not the |
| 241 | // file's own content, and the walk holds the same line. |
| 242 | let seen = match Seen::of(&full) { |
| 243 | Some(s) => s, |
| 244 | None => continue, |
| 245 | }; |
| 246 | let bytes = match fs::read(&full) { |
| 247 | Ok(b) => b, |
| 248 | Err(e) => return Err(err!(e, |
| 249 | "The file {:?} could not be read.", full; |
| 250 | IO, File, Read)), |
| 251 | }; |
| 252 | saw.insert(path.clone(), seen); |
| 253 | out.insert(path.clone(), bytes); |
| 254 | } |
| 255 | Ok((out, saw)) |
| 256 | } |
| 257 | |
| 258 | /// Stats every regular file the reading walk would have read, and reads none of |
| 259 | /// them. |
| 260 | /// |
| 261 | /// `known` is the path set the last scan produced, which stands in for the |
| 262 | /// tracked set: a tracked file is read whatever the ignore rules say, and |
| 263 | /// working that out from the history would mean the render this exists to |
| 264 | /// avoid. It is sound because the tracked set changes only when a capture |
| 265 | /// records something, and such a capture leaves no index behind. |
| 266 | pub fn survey<'a, I>(root: &Path, ignore: &Ignore, known: I) |
| 267 | -> Outcome<BTreeMap<Vec<u8>, Seen>> |
| 268 | where |
| 269 | I: IntoIterator<Item = &'a Vec<u8>>, |
| 270 | { |
| 271 | let mut saw = BTreeMap::new(); |
| 272 | res!(walk(root, &[], ignore, &mut |rel: &[u8], full: &Path| { |
| 273 | if let Some(seen) = Seen::of(full) { |
| 274 | saw.insert(rel.to_vec(), seen); |
| 275 | } |
| 276 | Ok(()) |
| 277 | })); |
| 278 | for path in known { |
| 279 | if saw.contains_key(path) { |
| 280 | continue; |
| 281 | } |
| 282 | let full = res!(tree::on_disk(root, path)); |
| 283 | if let Some(seen) = Seen::of(&full) { |
| 284 | saw.insert(path.clone(), seen); |
| 285 | } |
| 286 | } |
| 287 | Ok(saw) |
| 288 | } |
| 289 | |
| 290 | /// Walks one directory, recursing into those beneath it, and hands every |
| 291 | /// regular file it would read to `take`. |
| 292 | /// |
| 293 | /// ONE traversal, whatever is done with what it finds. [`scan`] reads the bytes |
| 294 | /// and [`survey`] does not, and the two cannot drift apart over which files the |
| 295 | /// working copy holds -- which they would, being the same rules written twice, |
| 296 | /// and the cost of that drift is an edit nobody records. |
| 297 | fn walk<F>(dir: &Path, prefix: &[u8], ignore: &Ignore, take: &mut F) |
| 298 | -> Outcome<()> |
| 299 | where |
| 300 | F: FnMut(&[u8], &Path) -> Outcome<()>, |
| 301 | { |
| 302 | let entries = match fs::read_dir(dir) { |
| 303 | Ok(e) => e, |
| 304 | Err(e) => return Err(err!(e, |
| 305 | "The directory {:?} could not be read.", dir; |
| 306 | IO, File, Read)), |
| 307 | }; |
| 308 | for entry in entries { |
| 309 | let entry = res!(entry); |
| 310 | let name = res!(tree::name_bytes(&entry.file_name())); |
| 311 | // The two directories the version control systems keep their own business |
| 312 | // in, and nothing else. Every other dot-named thing is the project's |
| 313 | // content: see the module note on why the skip is by name and not by |
| 314 | // first character. Skipped at any depth, not only at the root, because a |
| 315 | // store nested inside the working copy is another repository's and no |
| 316 | // more this one's content than the one at the top is. |
| 317 | if name == ORE_DIR.as_bytes() || name == GIT_DIR.as_bytes() { |
| 318 | continue; |
| 319 | } |
| 320 | let mut rel = prefix.to_vec(); |
| 321 | if !rel.is_empty() { |
| 322 | rel.push(b'/'); |
| 323 | } |
| 324 | rel.extend_from_slice(&name); |
| 325 | let kind = match entry.file_type() { |
| 326 | Ok(k) => k, |
| 327 | Err(e) => return Err(err!(e, |
| 328 | "The kind of {:?} could not be read.", entry.path(); |
| 329 | IO, File, Read)), |
| 330 | }; |
| 331 | if kind.is_dir() { |
| 332 | // An ignored directory is not descended into, so nothing beneath it |
| 333 | // can be re-included -- which is git's rule too. |
| 334 | if ignore.ignores(&rel, true) { |
| 335 | continue; |
| 336 | } |
| 337 | res!(walk(&entry.path(), &rel, ignore, take)); |
| 338 | continue; |
| 339 | } |
| 340 | // Only regular files are content. A symbolic link, a socket or a device |
| 341 | // is not something the vocabulary can say anything about yet. |
| 342 | if !kind.is_file() { |
| 343 | continue; |
| 344 | } |
| 345 | if ignore.ignores(&rel, false) { |
| 346 | continue; |
| 347 | } |
| 348 | res!(take(&rel, &entry.path())); |
| 349 | } |
| 350 | Ok(()) |
| 351 | } |
| 352 | |
| 353 | |
| 354 | /// Derives the operations that turn a rendered file into the given bytes. |
| 355 | /// |
| 356 | /// An empty list means the bytes are already what the render says they are. |
| 357 | /// The list is in ascending order of position in the old bytes, and it is meant |
| 358 | /// to be authored in that order: every anchor was resolved against this one |
| 359 | /// render, which is the old state, and no two of the splices touch the same |
| 360 | /// content. |
| 361 | pub fn derive(view: &Rendered, want: &[u8]) |
| 362 | -> Outcome<Vec<Op>> |
| 363 | { |
| 364 | let have = view.bytes(); |
| 365 | let mut out = Vec::new(); |
| 366 | for sp in diff::diff(have, want) { |
| 367 | out.push(res!(view.splice(sp.at, sp.delete, sp.insert))); |
| 368 | } |
| 369 | Ok(out) |
| 370 | } |
| 371 | |
| 372 | /// Returns the splice that fills a file just created, which is anchored at that |
| 373 | /// file's origin anchor. |
| 374 | /// |
| 375 | /// The origin anchor is the one byte of content [`Op::FileCreate`] mints, born |
| 376 | /// dead, so that an empty file is not empty in identifier space. It is what makes |
| 377 | /// the first insertion into a new file an ordinary splice binding after an |
| 378 | /// ordinary byte, and it is why nothing in a splice names a file. |
| 379 | pub fn fill(file: OpId, bytes: Vec<u8>) -> Op { |
| 380 | Op::Splice { |
| 381 | left: Some(Anchor::origin(file)), |
| 382 | right: None, |
| 383 | remove: Vec::new(), |
| 384 | insert: bytes.into(), |
| 385 | } |
| 386 | } |
| 387 | |
| 388 | /// One file a capture found changed, and what recording the change cost. |
| 389 | /// |
| 390 | /// The two counts are what the finer difference is for, so they are reported |
| 391 | /// rather than left to be inferred: a small edit to a large file is one or two |
| 392 | /// operations carrying the bytes that were typed. A count of no operations is |
| 393 | /// possible and truthful: a file whose whole change was one half of a move has |
| 394 | /// its operation authored under the other half's path. |
| 395 | #[derive(Clone, Debug)] |
| 396 | pub struct Edit { |
| 397 | /// The path it materialises under. |
| 398 | pub path: Vec<u8>, |
| 399 | /// How many operations the difference came to. |
| 400 | pub ops: usize, |
| 401 | /// How many bytes those operations insert between them. |
| 402 | pub inserted: usize, |
| 403 | } |
| 404 | |
| 405 | /// One block the capture recorded as a move rather than as a deletion and an |
| 406 | /// insertion. |
| 407 | #[derive(Clone, Debug)] |
| 408 | pub struct MovedBlock { |
| 409 | /// The path the bytes left. |
| 410 | pub from: Vec<u8>, |
| 411 | /// The path they landed in, which may be the same path. |
| 412 | pub to: Vec<u8>, |
| 413 | /// How many bytes moved. |
| 414 | pub bytes: usize, |
| 415 | } |
| 416 | |
| 417 | /// What a capture recorded. |
| 418 | /// |
| 419 | /// Neither cloned nor compared anywhere, and it now carries a whole render, so |
| 420 | /// it stays as cheap to move as it was. |
| 421 | #[derive(Debug, Default)] |
| 422 | pub struct Captured { |
| 423 | /// Files that did not exist before. |
| 424 | pub created: Vec<Vec<u8>>, |
| 425 | /// Files whose bytes changed. |
| 426 | pub edited: Vec<Edit>, |
| 427 | /// Files that are gone. |
| 428 | pub deleted: Vec<Vec<u8>>, |
| 429 | /// Blocks recorded as moves, in the order they were authored. |
| 430 | pub moved: Vec<MovedBlock>, |
| 431 | /// How many operations were appended. |
| 432 | pub ops: usize, |
| 433 | // The render capture worked from, where a verb can use it as its own. It is |
| 434 | // offered only when the capture appended nothing, so that it is still the |
| 435 | // present. Every verb that wants a tree was rendering a second one. |
| 436 | pub tree: Option<tree::Tree>, |
| 437 | } |
| 438 | |
| 439 | impl Captured { |
| 440 | /// Reports whether the working copy was already what the history said. |
| 441 | pub fn is_empty(&self) -> bool { |
| 442 | self.ops == 0 |
| 443 | } |
| 444 | } |
| 445 | |
| 446 | /// The positional difference of one changed file, held back until every file |
| 447 | /// has been diffed, because a move is a property of the capture as a whole: the |
| 448 | /// deletion it pairs may sit in one file and the insertion in another. |
| 449 | struct FileDiff { |
| 450 | /// The path the file materialises under. |
| 451 | path: Vec<u8>, |
| 452 | /// The file's identity. |
| 453 | file: OpId, |
| 454 | /// The ordered splices turning the render into the disk bytes. |
| 455 | splices: Vec<diff::Splice>, |
| 456 | } |
| 457 | |
| 458 | /// One whole-region move the pairing found: which file's splice list holds each |
| 459 | /// half, and where in it. Each half is `(file index, splice index)`. |
| 460 | struct Pair { |
| 461 | /// The pure deletion whose bytes moved. |
| 462 | del: (usize, usize), |
| 463 | /// The pure insertion they moved to. |
| 464 | ins: (usize, usize), |
| 465 | } |
| 466 | |
| 467 | /// Pairs removed regions with inserted regions under the rules of the module |
| 468 | /// doc: exact byte equality over at least [`MIN_MOVE_LEN`] bytes, one removal |
| 469 | /// to one insertion, whole regions only. |
| 470 | /// |
| 471 | /// Only a splice that purely deletes offers its bytes as a removal, and only |
| 472 | /// one that purely inserts offers them as an arrival; a splice that does both |
| 473 | /// is an edit, not a relocation. Grouping is by the bytes themselves, so a |
| 474 | /// region that vanished once and appeared twice -- or the reverse -- is seen as |
| 475 | /// the ambiguity it is, across every changed file at once, and left unpaired. |
| 476 | fn pair_moves(tree: &tree::Tree, diffs: &[FileDiff]) |
| 477 | -> Outcome<Vec<Pair>> |
| 478 | { |
| 479 | // The removal and arrival sites of each distinct block of bytes. |
| 480 | let mut sites: BTreeMap<Vec<u8>, (Vec<(usize, usize)>, Vec<(usize, usize)>)> = |
| 481 | BTreeMap::new(); |
| 482 | for (fi, fd) in diffs.iter().enumerate() { |
| 483 | let view = match tree.get(fd.file) { |
| 484 | Some(v) => v, |
| 485 | None => return Err(err!( |
| 486 | "The capture diffed the file {}, which the render does not hold.", |
| 487 | fd.file; |
| 488 | Bug, Missing)), |
| 489 | }; |
| 490 | let have = view.bytes(); |
| 491 | for (si, sp) in fd.splices.iter().enumerate() { |
| 492 | if sp.insert.is_empty() && sp.delete >= MIN_MOVE_LEN { |
| 493 | sites.entry(have[sp.at..sp.at + sp.delete].to_vec()) |
| 494 | .or_default().0.push((fi, si)); |
| 495 | } else if sp.delete == 0 && sp.insert.len() >= MIN_MOVE_LEN { |
| 496 | sites.entry(sp.insert.clone()) |
| 497 | .or_default().1.push((fi, si)); |
| 498 | } |
| 499 | } |
| 500 | } |
| 501 | let mut out: Vec<Pair> = Vec::new(); |
| 502 | for (dels, inss) in sites.into_values() { |
| 503 | if dels.len() == 1 && inss.len() == 1 { |
| 504 | out.push(Pair { del: dels[0], ins: inss[0] }); |
| 505 | } |
| 506 | } |
| 507 | Ok(out) |
| 508 | } |
| 509 | |
| 510 | /// Refuses the whole capture where a file about to be recorded holds a credential. |
| 511 | /// |
| 512 | /// Run before anything is authored rather than beside the authoring, because an operation reaches |
| 513 | /// the segment the moment it is written: a guard that refused half way would have recorded the |
| 514 | /// half it had already passed. Only files whose bytes differ from what the history says are looked |
| 515 | /// at, so the cost falls on what changed and not on the tree. See [`crate::guard`] for why the |
| 516 | /// remedy is a refusal and nothing else. |
| 517 | fn refuse_credentials( |
| 518 | tree: &tree::Tree, |
| 519 | layout: &tree::Layout, |
| 520 | disk: &BTreeMap<Vec<u8>, Vec<u8>>, |
| 521 | ) |
| 522 | -> Outcome<()> |
| 523 | { |
| 524 | let mut caught = Vec::new(); |
| 525 | for (path, want) in disk { |
| 526 | // Lockfiles and vendored trees carry long hashes that read like keys. |
| 527 | if secret::skip_path(path) { |
| 528 | continue; |
| 529 | } |
| 530 | let had = match layout.file_at(path) { |
| 531 | Some(file) => match tree.get(file) { |
| 532 | // Bytes that have not moved are bytes this command is not recording. |
| 533 | Some(view) if view.bytes() == want.as_slice() => continue, |
| 534 | Some(view) => Some(view.bytes()), |
| 535 | None => None, |
| 536 | }, |
| 537 | None => None, |
| 538 | }; |
| 539 | caught.extend(guard::inspect(path, want, had)); |
| 540 | } |
| 541 | guard::refuse(&caught) |
| 542 | } |
| 543 | |
| 544 | /// Is the working copy exactly what the index last measured, at this frontier? |
| 545 | /// |
| 546 | /// THE INDEX, and it is the only thing in a capture that does not read the disk. |
| 547 | /// Where every path is where it was, the size it was, the inode it was and the |
| 548 | /// times it was, and the history has not moved since, there is nothing to capture |
| 549 | /// -- so nothing is read and, more to the point, nothing is rendered. The render |
| 550 | /// is 100 MB of the 169 MB an `ore log` costs, and `ore log` never looks at a |
| 551 | /// file. |
| 552 | /// |
| 553 | /// `refuse_credentials` is not skipped so much as answered: it inspects only |
| 554 | /// files whose bytes differ from what the history says, and where this is true |
| 555 | /// none do, so it has nothing to catch. A path that arrived is a path the index |
| 556 | /// does not know, which is not this case. |
| 557 | /// |
| 558 | /// It is asked separately as well as by [`capture`], by a command that means to |
| 559 | /// answer without opening the log at all: see [`crate::listing`]. There is one |
| 560 | /// implementation of the question so that the two cannot come to disagree about |
| 561 | /// what "unchanged" is. |
| 562 | pub fn unchanged(root: &Path, dir: &Path, ignore: &Ignore, frontier: &[OpId]) |
| 563 | -> Outcome<bool> |
| 564 | { |
| 565 | let was = match Stats::read(dir) { |
| 566 | Some(w) => w, |
| 567 | None => return Ok(false), |
| 568 | }; |
| 569 | if !was.taken_at(frontier) { |
| 570 | return Ok(false); |
| 571 | } |
| 572 | let now = res!(survey(root, ignore, was.paths())); |
| 573 | Ok(was.agrees(&now)) |
| 574 | } |
| 575 | |
| 576 | /// Compares the working copy with the history and appends what differs. |
| 577 | /// |
| 578 | /// Operations are authored one at a time rather than gathered and written at the |
| 579 | /// end, because a file's creation is its identity: the splice that fills a new |
| 580 | /// file anchors at the origin anchor of the `FileCreate` that made it, and that |
| 581 | /// identity is not known until the create has been appended. |
| 582 | /// |
| 583 | /// Changed files are diffed before any of their operations are authored, so |
| 584 | /// that a removed region in one can be paired with an inserted region in |
| 585 | /// another and recorded as the one [`Op::Move`] it was. Every anchor of every |
| 586 | /// operation is resolved against the one render of the old state, which is what |
| 587 | /// makes the deferral safe: nothing authored here changes what an offset into |
| 588 | /// the old bytes names. |
| 589 | pub fn capture(repo: &mut Repo) |
| 590 | -> Outcome<Captured> |
| 591 | { |
| 592 | let ignore = res!(Ignore::read(&repo.root)); |
| 593 | // The clock, before a single file is looked at. Everything the index is |
| 594 | // allowed to believe is measured against this reading; see [`crate::stat`]. |
| 595 | let mut index = res!(Stats::beginning()); |
| 596 | let frontier = repo.log.frontier(); |
| 597 | |
| 598 | // Nothing to capture, and therefore nothing to render. See [`unchanged`]. |
| 599 | if res!(unchanged(&repo.root, &repo.dir, &ignore, &frontier)) { |
| 600 | return Ok(Captured::default()); |
| 601 | } |
| 602 | |
| 603 | let tree = res!(tree::whole(&repo.log)); |
| 604 | let layout = res!(tree.layout()); |
| 605 | let (disk, saw) = res!(scan(&repo.root, &ignore, layout.at.keys())); |
| 606 | res!(refuse_credentials(&tree, &layout, &disk)); |
| 607 | let replica = repo.cfg.replica; |
| 608 | let mut out = Captured::default(); |
| 609 | // Every path either side knows about, so that a file gone from the working |
| 610 | // copy is noticed as surely as one that arrived. A tracked path the scan did |
| 611 | // not find was read regardless of the rules, so its absence is a deletion. |
| 612 | let mut paths: Vec<Vec<u8>> = disk.keys().cloned().collect(); |
| 613 | for path in layout.at.keys() { |
| 614 | if !disk.contains_key(path) { |
| 615 | paths.push(path.clone()); |
| 616 | } |
| 617 | } |
| 618 | paths.sort(); |
| 619 | paths.dedup(); |
| 620 | let mut diffs: Vec<FileDiff> = Vec::new(); |
| 621 | for path in paths { |
| 622 | match (layout.file_at(&path), disk.get(&path)) { |
| 623 | (Some(file), Some(want)) => { |
| 624 | let view = match tree.get(file) { |
| 625 | Some(v) => v, |
| 626 | None => return Err(err!( |
| 627 | "The layout places the file {}, which the render does not hold.", |
| 628 | file; |
| 629 | Bug, Missing)), |
| 630 | }; |
| 631 | // Every offset the difference reports is an offset into the old |
| 632 | // bytes, and the render is the old state, so the whole list keeps |
| 633 | // until the authoring pass below. |
| 634 | let splices = diff::diff(view.bytes(), want); |
| 635 | if !splices.is_empty() { |
| 636 | diffs.push(FileDiff { path, file, splices }); |
| 637 | } |
| 638 | }, |
| 639 | (Some(file), None) => { |
| 640 | res!(repo.author(replica, Op::FileDelete { file })); |
| 641 | out.deleted.push(path.clone()); |
| 642 | out.ops += 1; |
| 643 | }, |
| 644 | (None, Some(want)) => { |
| 645 | let id = res!(repo.author(replica, Op::FileCreate { path: path.clone() })); |
| 646 | out.ops += 1; |
| 647 | if !want.is_empty() { |
| 648 | res!(repo.author(replica, fill(id, want.clone()))); |
| 649 | out.ops += 1; |
| 650 | } |
| 651 | out.created.push(path.clone()); |
| 652 | }, |
| 653 | (None, None) => (), |
| 654 | } |
| 655 | } |
| 656 | let pairs = res!(pair_moves(&tree, &diffs)); |
| 657 | // A pair is authored at whichever of its halves is reached first, and the |
| 658 | // other half is skipped when its turn comes. |
| 659 | let mut authored: Vec<bool> = vec![false; pairs.len()]; |
| 660 | for (fi, fd) in diffs.iter().enumerate() { |
| 661 | let view = match tree.get(fd.file) { |
| 662 | Some(v) => v, |
| 663 | None => return Err(err!( |
| 664 | "The capture diffed the file {}, which the render does not hold.", |
| 665 | fd.file; |
| 666 | Bug, Missing)), |
| 667 | }; |
| 668 | let mut edit = Edit { |
| 669 | path: fd.path.clone(), |
| 670 | ops: 0, |
| 671 | inserted: 0, |
| 672 | }; |
| 673 | for (si, sp) in fd.splices.iter().enumerate() { |
| 674 | let paired = pairs.iter().position(|p| p.del == (fi, si) || p.ins == (fi, si)); |
| 675 | match paired { |
| 676 | Some(pi) => { |
| 677 | if authored[pi] { |
| 678 | continue; |
| 679 | } |
| 680 | authored[pi] = true; |
| 681 | let del = &pairs[pi].del; |
| 682 | let ins = &pairs[pi].ins; |
| 683 | let dsp = &diffs[del.0].splices[del.1]; |
| 684 | let isp = &diffs[ins.0].splices[ins.1]; |
| 685 | let src = match tree.get(diffs[del.0].file) { |
| 686 | Some(v) => v, |
| 687 | None => return Err(err!( |
| 688 | "The move pairing names the file {}, which the render does \ |
| 689 | not hold.", diffs[del.0].file; |
| 690 | Bug, Missing)), |
| 691 | }; |
| 692 | // The source names the removed region's content identity through |
| 693 | // the render's runs, and the destination is the gap the inserted |
| 694 | // bytes were bound for; both are read off the old state. |
| 695 | let op = if del.0 == ins.0 { |
| 696 | res!(src.move_range(dsp.at, dsp.delete, isp.at)) |
| 697 | } else { |
| 698 | let dst = match tree.get(diffs[ins.0].file) { |
| 699 | Some(v) => v, |
| 700 | None => return Err(err!( |
| 701 | "The move pairing names the file {}, which the render \ |
| 702 | does not hold.", diffs[ins.0].file; |
| 703 | Bug, Missing)), |
| 704 | }; |
| 705 | res!(src.move_into(dsp.at, dsp.delete, dst, isp.at)) |
| 706 | }; |
| 707 | res!(repo.author(replica, op)); |
| 708 | out.ops += 1; |
| 709 | edit.ops += 1; |
| 710 | out.moved.push(MovedBlock { |
| 711 | from: diffs[del.0].path.clone(), |
| 712 | to: diffs[ins.0].path.clone(), |
| 713 | bytes: dsp.delete, |
| 714 | }); |
| 715 | }, |
| 716 | None => { |
| 717 | edit.inserted += sp.insert.len(); |
| 718 | res!(repo.author(replica, |
| 719 | res!(view.splice(sp.at, sp.delete, sp.insert.clone())))); |
| 720 | out.ops += 1; |
| 721 | edit.ops += 1; |
| 722 | }, |
| 723 | } |
| 724 | } |
| 725 | out.edited.push(edit); |
| 726 | } |
| 727 | // Handed on where the capture appended nothing, because then this render is |
| 728 | // still the present and the verb needs no second one. |
| 729 | // |
| 730 | // It is also the one moment the working copy is KNOWN to be what the history |
| 731 | // says, which is the only moment an index may be written. A capture that |
| 732 | // recorded something has moved the history and cannot say, without rendering |
| 733 | // again, that the disk now matches it -- so it writes no index and removes |
| 734 | // any there was, and the command after it pays one full capture to establish |
| 735 | // a fresh one. See [`crate::stat`]. |
| 736 | match out.ops { |
| 737 | 0 => { |
| 738 | out.tree = Some(tree); |
| 739 | index.describes(&frontier); |
| 740 | for (path, seen) in saw { |
| 741 | index.put(&path, seen); |
| 742 | } |
| 743 | // Derived state. A working copy that cannot be written to is one that |
| 744 | // captures the slow way, which is what every working copy did until |
| 745 | // this was written, so a failure here is not the command's. |
| 746 | let _ = index.write(&repo.dir); |
| 747 | }, |
| 748 | _ => Stats::forget(&repo.dir), |
| 749 | } |
| 750 | Ok(out) |
| 751 | } |