Oregami
Repositories/oxedyne/ore

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
114use crate::guard;
115use crate::repo::{
116 Repo,
117 GIT_DIR,
118 IGNORE_FILE,
119 ORE_DIR,
120};
121use crate::stat::{
122 Seen,
123 Stats,
124};
125use crate::tree;
126
127use oxedyne_fe2o3_core::prelude::*;
128use oxedyne_fe2o3_file::glob::IgnoreFile;
129use oxedyne_fe2o3_ore::diff;
130use oxedyne_fe2o3_ore::id::{
131 Anchor,
132 OpId,
133};
134use oxedyne_fe2o3_ore::op::Op;
135use oxedyne_fe2o3_ore::seq::render::Rendered;
136use oxedyne_fe2o3_text::secret;
137
138use std::collections::BTreeMap;
139use std::fs;
140use 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.
156pub 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)]
167pub struct Ignore {
168 /// The compiled rules, in the order the file gives them.
169 rules: IgnoreFile,
170}
171
172impl 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.
208pub fn scan<'a, I>(root: &Path, ignore: &Ignore, tracked: I)
209 -> Outcome<(BTreeMap<Vec<u8>, Vec<u8>>, BTreeMap<Vec<u8>, Seen>)>
210where
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.
266pub fn survey<'a, I>(root: &Path, ignore: &Ignore, known: I)
267 -> Outcome<BTreeMap<Vec<u8>, Seen>>
268where
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.
297fn walk<F>(dir: &Path, prefix: &[u8], ignore: &Ignore, take: &mut F)
298 -> Outcome<()>
299where
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.
361pub 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.
379pub 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)]
396pub 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)]
408pub 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)]
422pub 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
439impl 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.
449struct 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)`.
460struct 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.
476fn 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.
517fn 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.
562pub 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.
589pub 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}