OXYGENHUB

Coverage / crates/oxy-core/src/repo/snapshot.rs 90.7%

2236 of 2466 lines run, 292 of 383 functions · run #336 on paths-for-resolutions-and-amend · run missed

1
//! Working-copy snapshots, commits, and the change builder (spec §12.3).
2
3
use super::*;
4
use crate::objects::AtomRef;
5
6
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7
pub enum ChangeStatus {
8
    Added,
9
    Modified,
10
    Removed,
11
    /// Moved from another path (detected by similarity, tracked as a placement change).
12
    Renamed,
13
    ModeChanged,
14
    /// The file is conflicted in the base state and still contains conflict markers.
15
    ConflictEdited,
16
    /// The file was conflicted in the base state and the working copy resolves it.
17
    Resolved,
18
}
19
20
/// Lines that mark a conflict region in a materialized file (spec §9.5).
2133×
pub fn has_conflict_markers(content: &[u8]) -> bool {
2293×
    split_lines(content).iter().any(|l| {
2393×
        l.starts_with(b"<<<<<<< oxy conflict")
2488×
            || l.starts_with(b"======= oxy conflict")
2588×
            || l.starts_with(b">>>>>>> oxy conflict")
2693×
    })
2733×
}
28
29
#[derive(Clone, Debug, PartialEq, Eq)]
30
pub struct PathStatus {
31
    pub path: String,
32
    pub status: ChangeStatus,
33
}
34
35
/// Operations of a change built from the working directory against a base.
36
pub(crate) struct BuiltChange {
37
    pub tree_ops: Vec<TreeOp>,
38
    pub edits: Vec<Edit>,
39
    pub contributors: Vec<ChangeHash>,
40
    pub resolves: bool,
41
    pub statuses: Vec<PathStatus>,
42
    /// The stored blob of every opaque path, for the manifest's stat cache.
43
    pub blobs: HashMap<String, ObjectRef>,
44
}
45
46
/// Result of snapshotting the working copy.
47
pub struct SnapshotResult {
48
    /// The working-copy change's hash; for a commit's own snapshot, which
49
    /// does not encode it, the hash it had before.
50
    pub wc_hash: ChangeHash,
51
    pub wc: Change,
52
    /// The base state (closure of the working-copy change's dependencies).
53
    pub base: State,
54
    pub base_snapshot: Snapshot,
55
    pub statuses: Vec<PathStatus>,
56
    /// Whether the snapshot rewrote the working-copy change.
57
    pub changed: bool,
58
    /// Whether the working copy resolves conflicts of its base (spec §9.6).
59
    pub resolves: bool,
60
    /// Changes that contributed to the conflicts being resolved.
61
    pub contributors: Vec<ChangeHash>,
62
    /// The `.oxyattributes` in force on disk, from the files the walk read.
63
    pub(crate) attributes: crate::attributes::Attributes,
64
    /// Folders holding a repository of their own (`.git` or `.oxy`) that
65
    /// are not subrepos, so nothing in them is tracked (oxygen#43).
66
    pub untracked_nested: Vec<String>,
67
    /// A commit's snapshot leaves the manifest to the commit, which writes
68
    /// it once with the new base's graph keys.
69
    pub(crate) manifest: Option<PendingManifest>,
70
}
71
72
/// The manifest a commit's snapshot would have written, as of the moment
73
/// it would have written it (that moment is the racy-window stamp).
74
pub(crate) struct PendingManifest {
75
    entries: BTreeMap<String, FsEntry>,
76
    blobs: HashMap<String, ObjectRef>,
77
    dirs: Option<super::WalkedDirs>,
78
    written: u128,
79
}
80
8123058×
pub fn is_binary(content: &[u8]) -> bool {
8223058×
    content[..content.len().min(BINARY_SNIFF)].contains(&0)
8323058×
}
84
85
impl Repo {
86
    /// Diff the files on disk against `base` and build the operations of a
87
    /// change from `base` to the working directory. `reuse` maps paths to the
88
    /// NodeIds an earlier version of the same change assigned to new paths.
892×
    pub(crate) fn build_against(
902×
        &self,
912×
        base: &State,
922×
        base_snapshot: &Snapshot,
932×
        reuse: HashMap<String, (NodeId, NodeKind)>,
942×
    ) -> Result<BuiltChange> {
952×
        let mut fs_entries =
962×
            self.walk_working_copy_with(None, &Self::tracked_kinds(base_snapshot), None)?;
972×
        self.platform_normalize(&mut fs_entries, base_snapshot);
982×
        self.build_from_entries(base, base_snapshot, reuse, &fs_entries)
992×
    }
100
101
    /// Read the working copy as the platform left it (spec §7.7): without
102
    /// executable bits a file keeps the bit it has in the base, and without
103
    /// symbolic links a plain file where the base has a symlink is that
104
    /// symlink, written out as its target, not a new file.
1051336×
    fn platform_normalize(&self, entries: &mut BTreeMap<String, FsEntry>, base: &Snapshot) {
1061336×
        let p = self.platform;
1071336×
        if p.exec_bits && p.symlinks {
1081334×
            return;
1092×
        }
1104×
        for (path, e) in entries.iter_mut() {
1114×
            let Some(f) = base.files.get(path) else {
1120×
                continue;
113
            };
1144×
            if !p.symlinks && f.kind == NodeKind::Symlink && e.kind == NodeKind::File {
1152×
                e.kind = NodeKind::Symlink;
1162×
                e.exec = false;
1172×
            }
1184×
            if !p.exec_bits && f.kind == NodeKind::File && e.kind == NodeKind::File {
1192×
                e.exec = f.mode.exec;
1202×
            }
121
        }
1221336×
    }
123
124
    /// Like `build_against`, for files given in memory (imports).
1251063×
    pub(crate) fn build_from_entries(
1261063×
        &self,
1271063×
        base: &State,
1281063×
        base_snapshot: &Snapshot,
1291063×
        reuse: HashMap<String, (NodeId, NodeKind)>,
1301063×
        fs_entries: &BTreeMap<String, FsEntry>,
1311063×
    ) -> Result<BuiltChange> {
1321063×
        self.build_from_entries_resolving(base, base_snapshot, reuse, fs_entries, &HashSet::new())
1331063×
    }
134
135
    /// Like `build_from_entries`, settling the replace conflicts of the
136
    /// opaque files in `resolve` to their content (spec §9.3, §9.6): a
137
    /// caller that knows the user chose it, such as the hub's resolver. A
138
    /// snapshot never settles one on its own, since a checkout writes one
139
    /// candidate at the path.
1401078×
    pub(crate) fn build_from_entries_resolving(
1411078×
        &self,
1421078×
        base: &State,
1431078×
        base_snapshot: &Snapshot,
1441078×
        reuse: HashMap<String, (NodeId, NodeKind)>,
1451078×
        fs_entries: &BTreeMap<String, FsEntry>,
1461078×
        resolve: &HashSet<String>,
1471078×
    ) -> Result<BuiltChange> {
1481078×
        self.build_from_entries_with(
1491078×
            base,
1501078×
            base_snapshot,
1511078×
            reuse,
1521078×
            fs_entries,
1531078×
            resolve,
1541078×
            Scope::default(),
155
        )
1561078×
    }
157
158
    /// `build_from_entries` whose new nodes take identifiers derived from
159
    /// `seed` and their paths instead of random ones, so the same input
160
    /// always builds the same change (a Git import, spec §23).
161
    ///
162
    /// With `dir_needed`, the snapshot and entries hold only the paths the
163
    /// import touches (and the directories a removal may leave empty); a
164
    /// directory stays while `dir_needed` says the new tree has something
165
    /// under it.
16688×
    pub(crate) fn build_from_entries_seeded(
16788×
        &self,
16888×
        base: &State,
16988×
        base_snapshot: &Snapshot,
17088×
        fs_entries: &BTreeMap<String, FsEntry>,
17188×
        seed: [u8; 32],
17288×
        dir_needed: Option<&dyn Fn(&str) -> bool>,
17388×
    ) -> Result<BuiltChange> {
17488×
        self.build_from_entries_with(
17588×
            base,
17688×
            base_snapshot,
17788×
            HashMap::new(),
17888×
            fs_entries,
17988×
            &HashSet::new(),
18088×
            Scope {
18188×
                seed: Some(seed),
18288×
                dir_needed,
18388×
            },
184
        )
18588×
    }
186
187
    /// `build_from_entries` for a snapshot most of whose files are known
188
    /// unchanged (oxygen#41): only the paths that may differ are built
189
    /// (files read rather than known unchanged, base files gone from disk,
190
    /// and the directories those may leave empty), against a snapshot of
191
    /// just those, so a large tree with a few edits costs what they touch.
192884×
    fn build_changed_only(
193884×
        &self,
194884×
        base: &State,
195884×
        base_snapshot: &Snapshot,
196884×
        reuse: HashMap<String, (NodeId, NodeKind)>,
197884×
        fs_entries: &BTreeMap<String, FsEntry>,
198884×
    ) -> Result<BuiltChange> {
199884×
        let pick = crate::timing::phase("snapshot:   of which picking the changed");
200
        // A file known unchanged may still have changed mode: that alters
201
        // neither its size nor its time.
202884×
        let changed: BTreeMap<String, FsEntry> = join(fs_entries, &base_snapshot.files)
20311056×
            .filter_map(|(p, e, f)| {
20411056×
                let e = e?;
20510926×
                (!e.unchanged || f.is_none_or(|f| f.kind != e.kind || f.mode.exec != e.exec))
20610926×
                    .then(|| (p.clone(), e.clone()))
20711056×
            })
208884×
            .collect();
209884×
        let mut files: BTreeMap<String, crate::state::FileState> = BTreeMap::new();
2106697×
        for p in changed.keys() {
2116697×
            if let Some(f) = base_snapshot.files.get(p) {
2123686×
                files.insert(p.clone(), f.clone());
2133686×
            }
214
        }
215
        // Paths the base has and the disk does not.
216884×
        let gone = join(&base_snapshot.files, fs_entries)
21711056×
            .filter_map(|(p, f, e)| (e.is_none()).then_some((p, f?)));
218884×
        for (p, f) in gone {
219130×
            if f.kind == NodeKind::Dir {
22096×
                continue;
22134×
            }
22234×
            files.insert(p.clone(), f.clone());
22334×
            let mut end = p.len();
22439×
            while let Some(i) = p[..end].rfind('/') {
2255×
                let dir = &p[..i];
2265×
                if files.contains_key(dir) {
2270×
                    break;
2285×
                }
2295×
                if let Some(d) = base_snapshot.files.get(dir) {
2305×
                    files.insert(dir.to_string(), d.clone());
2315×
                }
2325×
                end = i;
233
            }
234
        }
235884×
        let part = Snapshot {
236884×
            tree: base_snapshot.tree.clone(),
237884×
            files,
238884×
            unrendered: BTreeSet::new(),
239884×
        };
240884×
        drop(pick);
241884×
        let _build = crate::timing::phase("snapshot:   of which building the change");
242
        // Whether anything on disk is under `dir`.
243884×
        let dir_needed = |dir: &str| {
2445×
            let prefix = format!("{dir}/");
2455×
            fs_entries
2465×
                .range(prefix.clone()..)
2475×
                .next()
2485×
                .is_some_and(|(k, _)| k.starts_with(&prefix))
2495×
        };
250884×
        self.build_from_entries_with(
251884×
            base,
252884×
            &part,
253884×
            reuse,
254884×
            &changed,
255884×
            &HashSet::new(),
256884×
            Scope {
257884×
                seed: None,
258884×
                dir_needed: Some(&dir_needed),
259884×
            },
260
        )
261884×
    }
262
2632050×
    fn build_from_entries_with(
2642050×
        &self,
2652050×
        base: &State,
2662050×
        base_snapshot: &Snapshot,
2672050×
        reuse: HashMap<String, (NodeId, NodeKind)>,
2682050×
        fs_entries: &BTreeMap<String, FsEntry>,
2692050×
        resolve: &HashSet<String>,
2702050×
        scope: Scope,
2712050×
    ) -> Result<BuiltChange> {
2722050×
        let Scope { seed, dir_needed } = scope;
2732050×
        let mut laps = crate::timing::Laps::start();
274
        // Attributes come from the files being committed, so a new
275
        // `.oxyattributes` applies to the same change that adds it.
2762050×
        let attrs = crate::attributes::Attributes::from_files(
2772050×
            fs_entries
2782050×
                .iter()
27918859×
                .filter(|(p, _)| p.as_str() == ".oxyattributes" || p.ends_with("/.oxyattributes"))
2802050×
                .map(|(p, e)| (p.as_str(), e.content.as_slice())),
281
        );
2822050×
        let mut b = ChangeBuilder::new(base, base_snapshot, reuse).with_attributes(attrs);
2832050×
        b.resolve = resolve.clone();
2842050×
        b.node_seed = seed;
2852050×
        laps.lap("build: builder");
286
        // Rename detection (spec §7.1, §23): a removed file and an added file
287
        // of the same kind whose contents are at least half alike become one
288
        // move of the same node, so identity survives across the rename.
2892050×
        let renames = detect_renames(base_snapshot, fs_entries, |p| self.in_scope(p));
2902050×
        let renamed_from: HashMap<&str, &str> = renames
2912050×
            .iter()
2922050×
            .map(|(old, new)| (new.as_str(), old.as_str()))
2932050×
            .collect();
2942050×
        let renamed_to: HashMap<&str, &str> = renames
2952050×
            .iter()
2962050×
            .map(|(old, new)| (old.as_str(), new.as_str()))
2972050×
            .collect();
2982050×
        laps.lap("build: renames");
2992050×
        b.diff_in_parallel(fs_entries);
3002050×
        laps.lap("build: diffs");
30118859×
        for (path, fs_entry) in fs_entries {
30218859×
            match base_snapshot.files.get(path) {
3036074×
                None => match renamed_from.get(path.as_str()) {
30433×
                    Some(old) => {
30533×
                        b.move_path(self, old, path, fs_entry, &base_snapshot.files[*old])?
306
                    }
3076041×
                    None => b.add_path(self, path, fs_entry)?,
308
                },
30912785×
                Some(expected) => b.update_path(self, path, fs_entry, expected)?,
310
            }
311
        }
3122050×
        laps.lap("build: paths");
313
        // A directory stays while anything under it exists or is out of
314
        // scope: the directories above each such path, gathered once.
3152050×
        let mut needed_dirs: HashSet<&str> = HashSet::new();
31618872×
        fn parents_of<'a>(p: &'a str, set: &mut HashSet<&'a str>) {
31718872×
            let mut end = p.len();
31819538×
            while let Some(i) = p[..end].rfind('/') {
31915448×
                if !set.insert(&p[..i]) {
32014782×
                    break;
321666×
                }
322666×
                end = i;
323
            }
32418872×
        }
32518859×
        for p in fs_entries.keys() {
32618859×
            parents_of(p, &mut needed_dirs);
32718859×
        }
32813356×
        for p in base_snapshot.files.keys() {
32913356×
            if !self.in_scope(p) {
33013×
                parents_of(p, &mut needed_dirs);
33113343×
            }
332
        }
33313356×
        for (path, expected) in &base_snapshot.files {
33413356×
            if fs_entries.contains_key(path)
335571×
                || !self.in_scope(path)
336558×
                || renamed_to.contains_key(path.as_str())
337
            {
33812831×
                continue;
339525×
            }
340525×
            let still_needed = expected.kind == NodeKind::Dir
341425×
                && match dir_needed {
34221×
                    Some(f) => f(path),
343404×
                    None => needed_dirs.contains(path.as_str()),
344
                };
345525×
            if !still_needed {
346123×
                b.remove_path(path, expected);
347402×
            }
348
        }
3492050×
        laps.lap("build: removals");
3502050×
        if seed.is_some() {
35188×
            b.retire_stray_placements();
3521962×
        }
3532050×
        laps.lap("build: stray placements");
3542050×
        let mut statuses = b.statuses;
3555670×
        statuses.sort_by(|a, b| a.path.cmp(&b.path));
3562050×
        Ok(BuiltChange {
3572050×
            tree_ops: b.tree_ops,
3582050×
            edits: b.edits,
3592050×
            contributors: b.contributors.into_iter().collect(),
3602050×
            resolves: b.resolves,
3612050×
            statuses,
3622050×
            blobs: b.blobs,
3632050×
        })
3642050×
    }
365
366
    /// Store a change and, when `core.sign = change` and we are its author,
367
    /// sign it. A rewritten change by another author is stored unsigned: only
368
    /// its author (or a delegate) can vouch for it.
369714×
    pub(crate) fn store_signed(&self, change: &Change) -> Result<ChangeHash> {
370714×
        let ours = change.author == self.author();
371714×
        if self.config.sign_changes && ours && self.secret_key()?.is_none() {
3721×
            return fail("core.sign is \"change\" but no secret key is available for author.key");
373713×
        }
374713×
        let storing = crate::timing::phase("store: the change");
375713×
        let hash = self.store.put_change(change)?;
376713×
        drop(storing);
377713×
        if self.config.sign_changes && ours {
37841×
            let _t = crate::timing::phase("store: sign it");
37941×
            self.sign_change(&hash)?;
380672×
        }
381713×
        Ok(hash)
382714×
    }
383
384
    /// Snapshot the working copy into the working-copy change (spec §12.3).
385
    /// The result carries the full base state and its whole rendering.
386450×
    pub fn snapshot(&self, command: &str) -> Result<SnapshotResult> {
387450×
        self.snapshot_as(command, false, false)
388450×
    }
389
390
    /// Snapshot the working copy as [`Repo::snapshot`] does, for commands
391
    /// that need its statuses, conflicts and base history, and the graphs
392
    /// of the files it changes: status, log, show, commit, diff, hunks and
393
    /// discard. Text files known unchanged since the last manifest are
394
    /// neither read nor rendered: the result's `base` is a history-only
395
    /// state without their graphs, and its `base_snapshot` lists them in
396
    /// `unrendered` with empty bytes. They are never conflicted, and no
397
    /// change the working copy makes touches them.
398422×
    pub fn snapshot_status(&self, command: &str) -> Result<SnapshotResult> {
399422×
        self.snapshot_as(command, true, false)
400422×
    }
401
402
    /// `snapshot_as`; `for_commit` writes the manifest even where a status
403
    /// would not bother (an edited file is read next time whatever it
404
    /// says), since after the commit that file is unchanged, and records no
405
    /// operation: the commit's own, recorded straight after, replaces the
406
    /// working copy, so the snapshot's would cost a durable write for
407
    /// nothing. Should the commit fail, the files are still on disk and the
408
    /// next snapshot finds them.
4091337×
    fn snapshot_as(
4101337×
        &self,
4111337×
        command: &str,
4121337×
        partial: bool,
4131337×
        for_commit: bool,
4141337×
    ) -> Result<SnapshotResult> {
4151337×
        let _lock = self.lock_snapshot()?;
4161337×
        let batch = self.store.begin_batch()?;
4171337×
        let result = self.snapshot_locked(command, partial, for_commit);
4181337×
        batch.finish()?;
4191337×
        result
4201337×
    }
421
4221337×
    fn snapshot_locked(
4231337×
        &self,
4241337×
        command: &str,
4251337×
        partial: bool,
4261337×
        for_commit: bool,
4271337×
    ) -> Result<SnapshotResult> {
4281337×
        let _all = crate::timing::phase("snapshot");
4291337×
        let (op_ref, op) = self.head_op()?;
4301337×
        let (old_hash, old_wc) = self.working_copy()?;
431
        // The base's history first: its graph keys say which files the
432
        // manifest already knows equal to it.
4331335×
        let mut base = {
4341335×
            let _t = crate::timing::phase("snapshot: load base history");
4351335×
            self.load_state_indexed(&old_wc.deps)?
436
        };
4371335×
        let view = {
4381335×
            let _t = crate::timing::phase("snapshot: tree view");
4391335×
            base.tree().view()
440
        };
4411335×
        let manifest = {
4421335×
            let _t = crate::timing::phase("snapshot: read manifest");
4431335×
            self.read_manifest()?.map(|(_, _, m)| m)
444
        };
4451335×
        let tracked: BTreeMap<String, (NodeKind, Option<ObjectRef>)> = {
4461335×
            let _t = crate::timing::phase("snapshot: tracked paths");
447
            // In the view's path order, so the map is built without searching.
4481335×
            view.entries
4491335×
                .iter()
4509566×
                .filter(|(_, e)| e.kind != NodeKind::Dir)
4519167×
                .map(|(p, e)| {
4529167×
                    let blob = if e.kind != NodeKind::File || e.mode.opaque {
453151×
                        base.live_blobs(&e.node).first().copied()
454
                    } else {
4559016×
                        None
456
                    };
4579167×
                    (p.clone(), (e.kind, blob))
4589167×
                })
4591335×
                .collect()
460
        };
461
        // A text file's graph key, worked out when the walk asks for it.
46212194×
        let key_of = |p: &str| -> Option<[u8; 32]> {
46312194×
            let e = view.entries.get(p)?;
4649068×
            if e.kind != NodeKind::File || e.mode.opaque {
465124×
                return None;
4668944×
            }
4678944×
            base.graph_key(&e.node)
46812194×
        };
4691334×
        let mut fs_entries = {
4701335×
            let _t = crate::timing::phase("snapshot: read working copy");
4711335×
            self.walk_working_copy_with(manifest.as_deref(), &tracked, Some(&key_of))?
472
        };
473
        // The keys of the files read, before building graphs drops them: a
474
        // file found equal to its base rendering keeps its key.
4751334×
        let keys: HashMap<String, [u8; 32]> = fs_entries
4761334×
            .iter()
47712224×
            .filter(|(_, e)| !e.unchanged && e.kind == NodeKind::File)
4787953×
            .filter_map(|(p, _)| Some((p.clone(), key_of(p)?)))
4791334×
            .collect();
4801334×
        let base_snapshot = {
4811334×
            let _t = crate::timing::phase("snapshot: load and render base");
4821334×
            if partial {
483887×
                let sets = crate::timing::phase("snapshot:   of which choosing graphs");
484
                // Every file but those on disk known unchanged.
485887×
                let need: Vec<(&str, NodeId)> = join(&view.entries, &fs_entries)
48611071×
                    .filter_map(|(p, v, f)| {
48711071×
                        let v = v?;
4888059×
                        (v.kind == NodeKind::File && !f.is_some_and(|f| f.unchanged))
4898059×
                            .then_some((p.as_str(), v.node))
49011071×
                    })
491887×
                    .collect();
492887×
                let render: std::collections::HashSet<&str> =
493887×
                    need.iter().map(|(p, _)| *p).collect();
494887×
                let need: Vec<NodeId> = need.iter().map(|(_, n)| *n).collect();
495887×
                drop(sets);
496
                {
497887×
                    let _t = crate::timing::phase("snapshot:   of which building graphs");
498887×
                    self.load_graphs_of(&mut base, &need)?;
499
                }
500887×
                let _t = crate::timing::phase("snapshot:   of which rendering");
501887×
                base.materialize_only(&render)
502
            } else {
503447×
                self.load_all_graphs(&mut base)?;
504447×
                base.materialize()
505
            }
506
        };
507
508
        // Reuse the NodeIds the previous working-copy version assigned to new
509
        // paths, so an unchanged working copy hashes identically (spec §12.3).
5101334×
        let reuse = added_node_paths(&old_wc, &base_snapshot);
5111334×
        self.platform_normalize(&mut fs_entries, &base_snapshot);
5121334×
        let built = {
5131334×
            let _t = crate::timing::phase("snapshot: diff against base");
5141334×
            if partial && self.config.sparse_paths.is_empty() {
515
                #[cfg(test)]
516607×
                let full =
517607×
                    self.build_from_entries(&base, &base_snapshot, reuse.clone(), &fs_entries)?;
518884×
                let built = self.build_changed_only(&base, &base_snapshot, reuse, &fs_entries)?;
519
                #[cfg(test)]
520
                {
521
                    // New nodes take random identifiers: compare them by
522
                    // order of appearance.
5231214×
                    let norm = |b: &BuiltChange| {
5241214×
                        let text = format!("{:?} {:?} {:?}", b.tree_ops, b.edits, b.statuses);
5251214×
                        let mut seen: Vec<String> = Vec::new();
5261214×
                        let mut out = String::new();
5271214×
                        let mut rest = text.as_str();
52820002×
                        while let Some(i) = rest.find("NodeId(") {
52918788×
                            out.push_str(&rest[..i]);
53018788×
                            let end = i + rest[i..].find(')').unwrap() + 1;
53118788×
                            let id = rest[i..end].to_string();
5329046032×
                            let n = seen.iter().position(|x| *x == id).unwrap_or_else(|| {
5337088×
                                seen.push(id);
5347088×
                                seen.len() - 1
5357088×
                            });
53618788×
                            out.push_str(&format!("N{n}"));
53718788×
                            rest = &rest[end..];
538
                        }
5391214×
                        out.push_str(rest);
5401214×
                        out
5411214×
                    };
542607×
                    assert_eq!(
543607×
                        norm(&built),
544607×
                        norm(&full),
545
                        "a changed-only build is the full one"
546
                    );
547
                }
548884×
                built
549
            } else {
550450×
                self.build_from_entries(&base, &base_snapshot, reuse, &fs_entries)?
551
            }
552
        };
553
        // Text files found equal to an unconflicted base rendering keep its
554
        // graph key, so the next snapshot need not read or render them.
555
        {
5563772×
            let changed: HashSet<&str> = built.statuses.iter().map(|s| s.path.as_str()).collect();
55712224×
            for (p, e) in fs_entries.iter_mut() {
55812224×
                if e.unchanged || e.kind != NodeKind::File || changed.contains(p.as_str()) {
5597953×
                    continue;
5604271×
                }
5614271×
                e.graph_key = match base_snapshot.files.get(p).map(|f| &f.content) {
562
                    Some(FileContent::Text {
5634159×
                        bytes,
5644159×
                        materialized,
5654159×
                    }) if !materialized.is_conflicted() && *bytes == *e.content => {
5664153×
                        keys.get(p).copied()
567
                    }
568118×
                    _ => None,
569
                };
570
            }
571
        }
572
5731334×
        let mut wc = Change {
5741334×
            version: crate::objects::CHANGE_VERSION,
5751334×
            change_id: old_wc.change_id,
5761334×
            deps: vec![],
5771334×
            author: self.author(),
5781334×
            hlc: old_wc.hlc,
5791334×
            message: old_wc.message.clone(),
5801334×
            tree_ops: built.tree_ops,
5811334×
            edits: built.edits,
5821334×
            kind: ChangeKind::WorkingCopy,
5831334×
        };
5841334×
        let base_heads = base.maximal(&old_wc.deps);
5851334×
        let mut deps: BTreeSet<ChangeHash> = base_heads.into_iter().collect();
5861334×
        deps.extend(referenced_changes(&wc, &base));
5871334×
        deps.extend(built.contributors.iter().copied());
5881334×
        wc.deps = deps.into_iter().collect();
5891334×
        wc.normalize();
5901334×
        let resolves = built.resolves;
5911334×
        let contributors = built.contributors;
5921334×
        let statuses = built.statuses;
593
        // Empty snapshot: hash the candidate before storing; only record an op on change.
5941334×
        let record = crate::timing::phase("snapshot: hash and record");
595
        // Encoded once, its texts stored a batch at a time as they are
596
        // made (a stored blob is not stored again); the change itself only
597
        // when it changed.
598
        // For a commit the change is neither encoded, stored nor recorded:
599
        // the commit stores its own, built from this one's operations, and
600
        // its texts with it. The hash it would have is not needed.
6011334×
        let (new_hash, changed, encoded) = if for_commit {
602465×
            (old_hash, true, Vec::new())
603
        } else {
604869×
            let encoded = wc.encode_streaming(|batch| self.store.put_texts(&batch))?;
605869×
            let new_hash = crate::hash::hash_object(crate::hash::ObjectType::Change, &encoded);
606869×
            (new_hash, new_hash != old_hash, encoded)
607
        };
6081334×
        if changed && !for_commit {
609303×
            self.store.put(crate::hash::ObjectType::Change, &encoded)?;
610303×
            let mut view = op.view.clone();
611303×
            view.workspaces.insert(self.workspace, new_hash);
612303×
            self.record_op(&[op_ref], &format!("{command}{SNAPSHOT_SUFFIX}"), view)?;
6131031×
        }
6141334×
        drop(record);
615
        // The prompt compares against this without recording anything.
6161334×
        let _t = crate::timing::phase("snapshot: write manifest");
6171334×
        let conflicted = base_snapshot.is_conflicted();
6181334×
        let uncommitted = !wc.tree_ops.is_empty() || !wc.edits.is_empty();
619
        // Rewriting the manifest pays only when it would teach the next
620
        // snapshot something: other flags, a file found equal to the base
621
        // (it need not be read again), a new blob, a path added or gone.
622
        // An edited file is read next time whatever it says.
6231334×
        let worth = match self.read_manifest()? {
624465×
            _ if for_commit => true,
625330×
            None => true,
626539×
            Some((c, u, m)) => {
627539×
                c != conflicted
628539×
                    || u != uncommitted
629304×
                    || m.entries.len() != fs_entries.len()
630388×
                    || fs_entries.iter().any(|(p, e)| {
631388×
                        !e.unchanged && (e.graph_key.is_some() || !m.entries.contains_key(p))
632388×
                    })
63349×
                    || built
63449×
                        .blobs
63549×
                        .iter()
63649×
                        .any(|(p, b)| m.entries.get(p).and_then(|e| e.blob) != Some(*b))
637
                    // With uncommitted work, the status cache below holds
638
                    // only while the manifest describes the edited files as
639
                    // they are now.
64066×
                    || (uncommitted && statuses.iter().any(|st| {
64166×
                        let Some(e) = fs_entries.get(&st.path) else {
6424×
                            return m.entries.contains_key(&st.path);
643
                        };
64462×
                        let Some(me) = m.entries.get(&st.path) else {
6450×
                            return true;
646
                        };
64762×
                        let hash = e
64862×
                            .content_hash
64962×
                            .unwrap_or_else(|| *blake3::hash(&e.content).as_bytes());
65062×
                        let stat = e.stat.or_else(|| {
6510×
                            fs::symlink_metadata(self.root.join(&st.path))
6520×
                                .ok()
6530×
                                .map(|md| (md.len(), super::prompt::mtime_ns(&md)))
6540×
                        });
655
                        // The executable bit too: a mode change moves
656
                        // neither size nor time.
65762×
                        me.hash != hash || me.exec != e.exec || stat != Some((me.size, me.mtime_ns))
65866×
                    }))
659
            }
660
        };
6611334×
        let attributes = crate::attributes::Attributes::from_files(
6621334×
            fs_entries
6631334×
                .iter()
66412224×
                .filter(|(p, _)| is_attributes(p))
6651334×
                .map(|(p, e)| (p.as_str(), e.content.as_slice())),
666
        );
6671334×
        let mut manifest = None;
6681334×
        if for_commit {
669465×
            manifest = Some(PendingManifest {
670465×
                entries: fs_entries,
671465×
                blobs: built.blobs.clone(),
672465×
                dirs: self.walked_dirs.take(),
673465×
                written: super::prompt::now_ns(),
674465×
            });
675465×
        } else {
676869×
            if worth {
677823×
                self.write_manifest_entries(conflicted, uncommitted, &fs_entries, &built.blobs)?;
67846×
            }
679869×
            self.write_status_cache((uncommitted && !conflicted).then_some(&new_hash), &statuses);
680
        }
6811334×
        Ok(SnapshotResult {
6821334×
            wc_hash: new_hash,
6831334×
            wc,
6841334×
            base,
6851334×
            base_snapshot,
6861334×
            statuses,
6871334×
            changed,
6881334×
            resolves,
6891334×
            contributors,
6901334×
            attributes,
6911334×
            untracked_nested: self.untracked_nested.borrow().clone(),
6921334×
            manifest,
6931334×
        })
6941337×
    }
695
696
    /// `oxy status --json` (spec §9.5): the changed paths and every conflict
697
    /// of the working copy's base with its kind, path, and contributing
698
    /// ChangeIds.
6997×
    pub fn status_json(&self, snap: &SnapshotResult) -> Result<String> {
7007×
        let heads = self.bookmark_heads()?;
7017×
        let base_snapshot = &snap.base_snapshot;
7027×
        let by_node = base_snapshot.tree.by_node();
7037×
        let path_of = |n: &NodeId| by_node.get(n).map(|e| e.path.clone());
7047×
        let id_of = |h: &ChangeHash| {
7054×
            snap.base
7064×
                .get(h)
7074×
                .map(|m| m.change_id.to_hex())
7084×
                .unwrap_or_else(|| h.to_hex())
7094×
        };
7107×
        let paths = json_array(snap.statuses.iter().map(|s| {
7114×
            format!(
712
                "{{\"path\":{},\"status\":{}}}",
7134×
                json_str(&s.path),
7144×
                json_str(&format!("{:?}", s.status))
715
            )
7164×
        }));
7177×
        let mut conflicts: Vec<String> = Vec::new();
7187×
        for (p, f) in &base_snapshot.files {
7197×
            if !f.is_conflicted() {
7205×
                continue;
7212×
            }
7222×
            let (kind, changes) = match &f.content {
723
                FileContent::Text { .. } => {
7242×
                    ("text", snap.base.conflict_regions(&f.node).contributors)
725
                }
7260×
                FileContent::Opaque { blobs } => {
7270×
                    ("replace", snap.base.blob_contributors(&f.node, blobs))
728
                }
7290×
                FileContent::Dir => continue,
730
            };
7312×
            let resolved = match snap
7322×
                .statuses
7332×
                .iter()
7342×
                .find(|s| s.path == *p)
7352×
                .map(|s| s.status)
736
            {
7371×
                Some(ChangeStatus::Resolved) => "true",
7380×
                Some(ChangeStatus::ConflictEdited) => "\"markers\"",
7391×
                _ => "false",
740
            };
7412×
            conflicts.push(format!(
742
                "{{\"kind\":{},\"path\":{},\"node\":{},\"changes\":{},\"resolved\":{resolved}}}",
7432×
                json_str(kind),
7442×
                json_str(p),
7452×
                json_str(&f.node.to_hex()),
7464×
                json_array(changes.iter().map(|h| json_str(&id_of(h))))
747
            ));
748
        }
7497×
        for c in &base_snapshot.tree.conflicts {
7500×
            let (kind, nodes): (&str, Vec<NodeId>) = match c {
7510×
                crate::tree::TreeConflict::Name { nodes, .. } => ("name", nodes.clone()),
7520×
                crate::tree::TreeConflict::MultiPlacement { node, .. } => {
7530×
                    ("multi-placement", vec![*node])
754
                }
7550×
                crate::tree::TreeConflict::Orphan { node, .. } => ("orphan", vec![*node]),
7560×
                crate::tree::TreeConflict::Cycle { node } => ("cycle", vec![*node]),
7570×
                crate::tree::TreeConflict::Mode { node, .. } => ("mode", vec![*node]),
758
            };
7590×
            let detail = match c {
7600×
                crate::tree::TreeConflict::Name { name, parent, .. } => {
7610×
                    let dir = if *parent == NodeId::ROOT {
7620×
                        String::new()
763
                    } else {
7640×
                        path_of(parent).unwrap_or_default()
765
                    };
7660×
                    format!(
767
                        ",\"path\":{}",
7680×
                        json_str(&if dir.is_empty() {
7690×
                            name.clone()
770
                        } else {
7710×
                            format!("{dir}/{name}")
772
                        })
773
                    )
774
                }
7750×
                _ => nodes
7760×
                    .first()
7770×
                    .and_then(path_of)
7780×
                    .map(|p| format!(",\"path\":{}", json_str(&p)))
7790×
                    .unwrap_or_default(),
780
            };
7810×
            conflicts.push(format!(
782
                "{{\"kind\":{},\"nodes\":{}{detail}}}",
7830×
                json_str(kind),
7840×
                json_array(nodes.iter().map(|n| json_str(&n.to_hex())))
785
            ));
786
        }
7877×
        Ok(format!(
788
            "{{\"bookmark\":{},\"working_copy\":{},\"heads\":{},\"clean\":{},\"conflicted\":{},\"paths\":{paths},\"conflicts\":{},\"untracked_nested\":{},\"skipped_large\":{}}}",
7897×
            json_str(&self.config.bookmark),
7907×
            json_str(&snap.wc.change_id.to_hex()),
7917×
            heads.len(),
7927×
            snap.statuses.is_empty(),
7937×
            base_snapshot.is_conflicted(),
7947×
            json_array(conflicts),
7957×
            json_array(snap.untracked_nested.iter().map(|p| json_str(p))),
7967×
            json_array(
7977×
                self.skipped_large
7987×
                    .borrow()
7997×
                    .iter()
8007×
                    .map(|(p, n)| format!("{{\"path\":{},\"size\":{n}}}", json_str(p)))
801
            )
802
        ))
8037×
    }
804
805
    /// Turn the working-copy change into a normal change on the bookmark (spec §12.3).
806650×
    pub fn commit(&self, message: &str, extra_deps: &[ChangeHash]) -> Result<ChangeHash> {
807650×
        self.commit_selection(message, extra_deps, &Selection::All)
808650×
    }
809
810
    /// The hunks of the working copy, for `oxy commit --interactive`: each
811
    /// tree operation and each edit, in the order `select_hunks` numbers them.
8127×
    pub fn hunks(&self) -> Result<(SnapshotResult, Vec<Hunk>)> {
8137×
        let snap = self.snapshot_status("oxy commit")?;
8147×
        let hunks = render_hunks(&snap.wc, &snap.base, &snap.base_snapshot);
8157×
        Ok((snap, hunks))
8167×
    }
817
818
    /// Commit part of the working copy (spec §12.3). What is not selected
819
    /// stays on disk as uncommitted work: the next snapshot rebuilds it
820
    /// against the new base, so the working copy always describes the
821
    /// directory.
822
    /// The graph key in `st` of each text file at `paths`, the paths a
823
    /// commit changed: only their graphs changed, so the keys the manifest
824
    /// holds for the others still stand (and any that does not is simply
825
    /// not trusted by the next snapshot, which compares keys). Files with
826
    /// an `eol` attribute are left out: their bytes on disk are not the
827
    /// rendering.
828670×
    fn committed_graph_keys(
829670×
        &self,
830670×
        st: &State,
831670×
        paths: &[String],
832670×
        attrs: &crate::attributes::Attributes,
833670×
    ) -> HashMap<String, [u8; 32]> {
834670×
        let _t = crate::timing::phase("commit: graph keys");
835670×
        let view = st.tree().view();
836670×
        paths
837670×
            .iter()
8383504×
            .filter_map(|p| {
8393504×
                let e = view.entries.get(p)?;
8403468×
                (e.kind == NodeKind::File && !e.mode.opaque && attrs.lookup(p).eol.is_none())
8413468×
                    .then_some(())?;
8423418×
                Some((p.clone(), st.graph_key(&e.node)?))
8433504×
            })
844670×
            .collect()
845670×
    }
846
847655×
    pub fn commit_selection(
848655×
        &self,
849655×
        message: &str,
850655×
        extra_deps: &[ChangeHash],
851655×
        selection: &Selection,
852655×
    ) -> Result<ChangeHash> {
853655×
        Ok(self
854655×
            .commit_selection_partial(message, extra_deps, selection)?
855
            .0)
856655×
    }
857
858
    /// As [`Repo::commit_selection`], and whether the commit left part of
859
    /// the working copy uncommitted: when it did not, nothing is left, and
860
    /// no snapshot is needed to say so.
861684×
    pub fn commit_selection_partial(
862684×
        &self,
863684×
        message: &str,
864684×
        extra_deps: &[ChangeHash],
865684×
        selection: &Selection,
866684×
    ) -> Result<(ChangeHash, bool)> {
867
        // The commit's objects are written together, made durable at once
868
        // before the operation that names them (spec §13.6).
869684×
        let batch = self.store.begin_batch()?;
870684×
        let out = self.commit_selection_batched(message, extra_deps, selection);
871684×
        batch.finish()?;
872684×
        out
873684×
    }
874
875
    /// Write the manifest a commit's snapshot left to the commit, with the
876
    /// graph keys `keys` sets.
877454×
    fn write_pending_manifest(
878454×
        &self,
879454×
        mut m: PendingManifest,
880454×
        conflicted: bool,
881454×
        keys: HashMap<String, [u8; 32]>,
882454×
    ) -> Result<()> {
8832844×
        for (path, k) in keys {
8842844×
            if let Some(e) = m.entries.get_mut(&path) {
8852844×
                e.graph_key = Some(k);
8862844×
            }
887
        }
888454×
        self.write_manifest_entries_as_of(
889454×
            conflicted,
890
            false,
891454×
            &m.entries,
892454×
            &m.blobs,
893454×
            m.written,
894454×
            m.dirs.unwrap_or_default(),
895
        )
896454×
    }
897
898684×
    fn commit_selection_batched(
899684×
        &self,
900684×
        message: &str,
901684×
        extra_deps: &[ChangeHash],
902684×
        selection: &Selection,
903684×
    ) -> Result<(ChangeHash, bool)> {
904684×
        let _wc = self.lock_working_copy()?;
905684×
        let mut snap = match self.snapshot_from_cache()? {
906219×
            Some(snap) => snap,
907465×
            None => self.snapshot_as("oxy commit", true, true)?,
908
        };
909684×
        let pending = snap.manifest.take();
910684×
        if snap.wc.tree_ops.is_empty() && snap.wc.edits.is_empty() {
9113×
            if let Some(m) = pending {
9123×
                let conflicted = snap.base_snapshot.is_conflicted();
9133×
                self.write_pending_manifest(m, conflicted, HashMap::new())?;
9140×
            }
9153×
            return fail("nothing to commit: the working copy matches its base");
916681×
        }
917681×
        if let Some(s) = snap
918681×
            .statuses
919681×
            .iter()
9203527×
            .find(|s| s.status == ChangeStatus::ConflictEdited)
921
        {
9220×
            return fail(format!("{} still contains conflict markers", s.path));
923681×
        }
924681×
        if message.trim().is_empty() {
9250×
            return fail("a commit message is required");
926681×
        }
927
        // What the whole working copy references, before a whole commit
928
        // takes its operations over (they are moved, not copied: for a
929
        // large tree they are most of the memory).
930681×
        let wc_refs = referenced_changes(&snap.wc, &snap.base);
9313527×
        let changed_paths: Vec<String> = snap.statuses.iter().map(|s| s.path.clone()).collect();
932681×
        let (tree_ops, edits, partial) = match selection {
933670×
            Selection::All => (
934670×
                std::mem::take(&mut snap.wc.tree_ops),
935670×
                std::mem::take(&mut snap.wc.edits),
936670×
                false,
937670×
            ),
9387×
            Selection::Paths(paths) => {
9397×
                let (o, e) = select_paths(&snap.wc, &snap.base_snapshot, paths);
9407×
                let partial = o.len() != snap.wc.tree_ops.len() || e.len() != snap.wc.edits.len();
9417×
                (o, e, partial)
942
            }
9434×
            Selection::Hunks(indices) => {
9444×
                let (o, e) = select_hunks(&snap.wc, indices);
9454×
                let partial = o.len() != snap.wc.tree_ops.len() || e.len() != snap.wc.edits.len();
9464×
                (o, e, partial)
947
            }
948
        };
949681×
        if tree_ops.is_empty() && edits.is_empty() {
9500×
            return fail("nothing selected to commit");
951681×
        }
952
        // A resolution may leave other work uncommitted (oxygen#118), but
953
        // takes every file it resolves, whole.
954681×
        if partial && snap.resolves {
9552×
            let resolved = snap
9562×
                .statuses
9572×
                .iter()
9584×
                .filter(|s| s.status == ChangeStatus::Resolved)
9592×
                .map(|s| s.path.as_str());
9602×
            let missing: Vec<&str> = match selection {
9612×
                Selection::Paths(paths) => resolved
9622×
                    .filter(|p| {
9632×
                        !paths.iter().any(|x| {
9642×
                            p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/')))
9652×
                        })
9662×
                    })
9672×
                    .collect(),
9680×
                _ => resolved.collect(),
969
            };
9702×
            if !missing.is_empty() {
9711×
                return fail(format!(
972
                    "a conflict resolution takes every file it resolves, whole: commit by paths, including {}",
9731×
                    missing.join(", ")
974
                ));
9751×
            }
976679×
        }
977
        // pre-commit hook: may veto.
978680×
        self.run_hook(
979680×
            "pre-commit",
980680×
            &format!(
981
                "{{\"event\":\"pre-commit\",\"message\":{},\"paths\":{}}}",
982680×
                json_str(message),
9833525×
                json_array(snap.statuses.iter().map(|s| format!(
984
                    "{{\"path\":{},\"status\":{}}}",
9853525×
                    json_str(&s.path),
9863525×
                    json_str(&format!("{:?}", s.status))
987
                )))
988
            ),
9891×
        )?;
990679×
        let (op_ref, op) = self.head_op()?;
991679×
        let mut deps: BTreeSet<ChangeHash> = wc_refs;
992
        // semantic-deps hook: may add explicit dependencies (spec §8.1).
993679×
        if self.config.semantic_deps {
9941×
            let touched: Vec<String> = snap.statuses.iter().map(|s| s.path.clone()).collect();
9951×
            let by_node = snap.base_snapshot.tree.by_node();
9961×
            let mut candidates: Vec<(ChangeId, ChangeHash, Vec<String>)> = Vec::new();
9971×
            let mut seen: BTreeSet<ChangeHash> = BTreeSet::new();
9982×
            for h in deps.iter().chain(snap.base.maximal(&snap.wc.deps).iter()) {
9992×
                if !seen.insert(*h) {
10000×
                    continue;
10012×
                }
10022×
                let c = self.store.get_change(h)?;
10032×
                let mut paths: BTreeSet<String> = BTreeSet::new();
10046×
                for n in c
10052×
                    .tree_ops
10062×
                    .iter()
10073×
                    .map(|o| o.node())
10083×
                    .chain(c.edits.iter().map(|e| e.node()))
1009
                {
10106×
                    if let Some(e) = by_node.get(&n) {
10116×
                        paths.insert(e.path.clone());
10126×
                    }
1013
                }
10142×
                candidates.push((c.change_id, *h, paths.into_iter().collect()));
1015
            }
10161×
            for d in self.semantic_deps(&snap.wc.change_id, &touched, &candidates)? {
10171×
                if snap.base.contains(&d) {
10181×
                    deps.insert(d);
10191×
                }
1020
            }
1021678×
        }
1022
        // A resolution depends on every change that contributed to the
1023
        // conflict (spec §8.1). Tree and opaque conflicts are resolved through
1024
        // ordinary operations whose references already cover the contributors.
1025679×
        deps.extend(snap.contributors.iter().copied());
1026679×
        for d in extra_deps {
10274×
            if !snap.base.contains(d) {
10280×
                return fail(format!(
1029
                    "--depends-on {d}: not in the working copy's base state"
1030
                ));
10314×
            }
10324×
            deps.insert(*d);
1033
        }
1034679×
        let mut change = Change {
1035
            version: crate::objects::CHANGE_VERSION,
1036679×
            change_id: if partial {
10378×
                ChangeId::random()
1038
            } else {
1039671×
                snap.wc.change_id
1040
            },
1041679×
            deps: vec![],
1042679×
            author: self.author(),
1043679×
            hlc: Hlc::now(),
1044679×
            message: message.trim_end().to_string(),
1045679×
            tree_ops,
1046679×
            edits,
1047679×
            kind: if snap.resolves {
10488×
                ChangeKind::Resolution
1049
            } else {
1050671×
                ChangeKind::Normal
1051
            },
1052
        };
1053679×
        if partial {
1054
            // Dependencies come from what the selected part references, not
1055
            // from the whole working copy.
10568×
            deps = referenced_changes(&change, &snap.base);
10578×
            for d in extra_deps {
10581×
                deps.insert(*d);
10591×
            }
1060671×
        }
1061679×
        change.deps = deps.into_iter().collect();
1062679×
        change.normalize();
1063679×
        let signing = crate::timing::phase("commit: sign and store");
1064679×
        let hash = self.store_signed(&change)?;
1065678×
        drop(signing);
1066678×
        let extending = crate::timing::phase("commit: extend the state");
1067
1068
        // New bookmark heads: maximal elements of old heads plus the new change.
1069
        // The working copy continues on its own base plus the new change, which
1070
        // is exactly what the files on disk contain; the bookmark may hold
1071
        // other heads (and conflicts) the working copy has not checked out.
1072678×
        let old_heads = self.heads_of(&op.view, &self.config.bookmark)?;
1073678×
        let old_base = snap.base.maximal(&snap.wc.deps);
1074678×
        let mut all: Vec<ChangeHash> = old_heads.clone();
1075678×
        all.extend(old_base.iter().copied());
1076678×
        all.push(hash);
1077
        // The snapshot already loaded the working copy's base; when that also
1078
        // holds every bookmark head, it is the state to extend.
1079678×
        let mut st = if old_heads.iter().all(|h| snap.base.contains(h)) {
1080640×
            snap.base
1081
        } else {
108238×
            self.load_state(&all)?
1083
        };
1084678×
        st.add_hashed(hash, change)?;
1085678×
        let mut with_new = old_heads.clone();
1086678×
        with_new.push(hash);
1087678×
        let new_heads = st.maximal(&with_new);
1088
        // Automatic checkpoint once enough changes accumulated (spec §11).
1089678×
        let since = self
1090678×
            .latest_checkpoint(&self.config.bookmark)?
1091678×
            .map(|(_, cp)| cp.count)
1092678×
            .unwrap_or(0);
1093678×
        if self.config.checkpoint_interval > 0
1094678×
            && (st.len() as u64).saturating_sub(since) >= self.config.checkpoint_interval
1095
        {
10962×
            let cp_state = self.load_state_full(&new_heads)?;
10972×
            self.write_checkpoint(&self.config.bookmark, &cp_state, &new_heads)?;
1098676×
        }
1099678×
        let mut wc_base = old_base.clone();
1100678×
        wc_base.push(hash);
1101678×
        let wc_base = st.maximal(&wc_base);
1102678×
        drop(extending);
1103
1104
        // The graph keys of the files as they now are, from the state the
1105
        // commit just extended: a file whose graph it built has none, and
1106
        // is read by the next snapshot; a key the next base does not share
1107
        // (the bookmark held other heads) is simply not trusted then.
1108678×
        let keying = crate::timing::phase("commit: graph keys of the files");
1109678×
        let committed_keys =
1110678×
            (!partial).then(|| self.committed_graph_keys(&st, &changed_paths, &snap.attributes));
1111678×
        drop(keying);
1112678×
        let recording = crate::timing::phase("commit: record");
1113678×
        let wc = self.new_working_copy(wc_base, "");
1114678×
        let wc_hash = self.store.put_change(&wc)?;
1115678×
        let mut view = op.view.clone();
1116678×
        view.bookmarks.insert(
1117678×
            self.config.bookmark.clone(),
1118678×
            self.store.put_state(&StateObject::new(new_heads))?,
1119
        );
1120678×
        view.workspaces.insert(self.workspace, wc_hash);
1121678×
        self.record_op(&[op_ref], &format!("oxy commit -m {message:?}"), view)?;
1122673×
        drop(recording);
1123673×
        let rewriting = crate::timing::phase("commit: manifest");
1124673×
        if partial {
11258×
            // The rest is rebuilt from disk by the next snapshot.
11268×
            self.drop_manifest();
11278×
        } else {
1128665×
            let conflicted = snap.base_snapshot.is_conflicted() && !snap.resolves;
1129
            // The files on disk are now exactly the new base's renderings:
1130
            // record their graph keys, so the next snapshot neither reads nor
1131
            // renders them (oxygen#35).
1132665×
            let keys = if conflicted {
11330×
                HashMap::new()
1134
            } else {
1135665×
                committed_keys.unwrap_or_default()
1136
            };
1137665×
            match pending {
1138451×
                Some(m) => self.write_pending_manifest(m, conflicted, keys)?,
1139214×
                None => self.rewrite_manifest(conflicted, false, &keys)?,
1140
            }
1141
        }
1142673×
        drop(rewriting);
1143673×
        let packing = crate::timing::phase("commit: autopack");
1144673×
        self.maybe_autopack()?;
1145673×
        drop(packing);
1146673×
        let _hook = crate::timing::phase("commit: hook");
1147673×
        let _ = self.run_hook(
1148673×
            "post-commit",
1149673×
            &format!(
1150673×
                "{{\"event\":\"post-commit\",\"change_id\":{},\"hash\":{},\"message\":{}}}",
1151673×
                json_str(&snap.wc.change_id.to_hex()),
1152673×
                json_str(&hash.to_hex()),
1153673×
                json_str(message)
1154673×
            ),
1155673×
        );
1156673×
        Ok((hash, partial))
1157684×
    }
1158
}
1159
1160
/// What part of the working copy a commit takes (spec §12.3).
1161
#[derive(Clone, Debug)]
1162
pub enum Selection {
1163
    All,
1164
    /// Every operation and edit on nodes under these paths.
1165
    Paths(Vec<String>),
1166
    /// Hunk indices as `Repo::hunks` numbers them; dependent hunks are added.
1167
    Hunks(Vec<usize>),
1168
}
1169
1170
/// One selectable piece of the working copy: a tree operation or an edit.
1171
#[derive(Clone, Debug)]
1172
pub struct Hunk {
1173
    pub index: usize,
1174
    pub path: String,
1175
    /// Rendered like `oxy diff`.
1176
    pub text: String,
1177
}
1178
1179
/// The tree operations and edits on nodes under `paths`, plus the
1180
/// additions of any new parent directories they need.
11817×
fn select_paths(
11827×
    wc: &Change,
11837×
    base_snapshot: &Snapshot,
11847×
    paths: &[String],
11857×
) -> (Vec<TreeOp>, Vec<Edit>) {
118624×
    let selected = |p: &str| {
118724×
        paths
118824×
            .iter()
118924×
            .any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/'))))
119024×
    };
11917×
    let by_node = base_snapshot.tree.by_node();
11927×
    let mut new_names: HashMap<NodeId, (NodeId, String, NodeKind)> = HashMap::new();
11937×
    for op in &wc.tree_ops {
1194
        if let TreeOp::AddNode {
11954×
            node,
11964×
            parent,
11974×
            name,
11984×
            kind,
1199
            ..
12004×
        } = op
12014×
        {
12024×
            new_names.insert(*node, (*parent, name.clone(), *kind));
12034×
        }
1204
    }
12057×
    let mut nodes: BTreeSet<NodeId> = BTreeSet::new();
120624×
    for n in wc
12077×
        .tree_ops
12087×
        .iter()
12097×
        .map(|o| o.node())
121020×
        .chain(wc.edits.iter().map(|e| e.node()))
1211
    {
121224×
        if path_of_node(&n, &by_node, &new_names).is_some_and(|p| selected(&p)) {
121313×
            nodes.insert(n);
121413×
        }
1215
    }
1216
    // New parents of selected new nodes come along.
12177×
    let mut changed = true;
121814×
    while changed {
12197×
        changed = false;
12207×
        for (n, (parent, _, _)) in &new_names {
12214×
            if nodes.contains(n) && new_names.contains_key(parent) && nodes.insert(*parent) {
12220×
                changed = true;
12234×
            }
1224
        }
1225
    }
12267×
    let ops = wc
12277×
        .tree_ops
12287×
        .iter()
12297×
        .filter(|o| nodes.contains(&o.node()))
12307×
        .cloned()
12317×
        .collect();
12327×
    let edits = wc
12337×
        .edits
12347×
        .iter()
123520×
        .filter(|e| nodes.contains(&e.node()))
12367×
        .cloned()
12377×
        .collect();
12387×
    (ops, edits)
12397×
}
1240
1241
/// The hunks `indices` name plus whatever they refer to within the working
1242
/// copy (the parent directory of an added node, the insert that introduced
1243
/// an anchor line), renumbered so the result is a well-formed change.
12445×
fn select_hunks(wc: &Change, indices: &[usize]) -> (Vec<TreeOp>, Vec<Edit>) {
12455×
    let n_ops = wc.tree_ops.len();
12465×
    let mut ops_sel = vec![false; n_ops];
12475×
    let mut edits_sel = vec![false; wc.edits.len()];
12485×
    for &i in indices {
12494×
        if i < n_ops {
12501×
            ops_sel[i] = true;
12513×
        } else if i - n_ops < wc.edits.len() {
12523×
            edits_sel[i - n_ops] = true;
12533×
        }
1254
    }
1255
    // Atom ranges each insert introduces, per node, in the working copy's numbering.
12565×
    let mut per_node: HashMap<NodeId, u32> = HashMap::new();
12575×
    let mut insert_ranges: Vec<Option<(NodeId, u32, u32)>> = Vec::new();
125810×
    for e in &wc.edits {
125910×
        insert_ranges.push(match e {
126010×
            Edit::Insert { node, lines, .. } => {
126110×
                let start = per_node.entry(*node).or_default();
126210×
                let r = (*node, *start, *start + lines.len() as u32);
126310×
                *start += lines.len() as u32;
126410×
                Some(r)
1265
            }
12660×
            _ => None,
1267
        });
1268
    }
12695×
    let add_index: HashMap<NodeId, usize> = wc
12705×
        .tree_ops
12715×
        .iter()
12725×
        .enumerate()
12738×
        .filter_map(|(i, o)| {
12748×
            if let TreeOp::AddNode { node, .. } = o {
12758×
                Some((*node, i))
1276
            } else {
12770×
                None
1278
            }
12798×
        })
12805×
        .collect();
12815×
    let insert_for = |node: &NodeId, index: u32| -> Option<usize> {
12820×
        insert_ranges
12830×
            .iter()
12840×
            .position(|r| matches!(r, Some((n, a, b)) if n == node && *a <= index && index < *b))
12850×
    };
1286
    // Closure.
12875×
    let mut changed = true;
128813×
    while changed {
12898×
        changed = false;
129017×
        for i in 0..n_ops {
129117×
            if !ops_sel[i] {
129211×
                continue;
12936×
            }
12946×
            if let TreeOp::AddNode { node, parent, .. } = &wc.tree_ops[i] {
12956×
                if let Some(&p) = add_index.get(parent) {
12964×
                    if !ops_sel[p] {
12972×
                        ops_sel[p] = true;
12982×
                        changed = true;
12992×
                    }
13002×
                }
1301
                // A new node's content belongs with its addition: nobody
1302
                // wants an empty file committed apart from its lines.
130312×
                for (j, e) in wc.edits.iter().enumerate() {
130412×
                    if e.node() == *node && !edits_sel[j] {
13051×
                        edits_sel[j] = true;
13061×
                        changed = true;
130711×
                    }
1308
                }
13090×
            }
1310
        }
131116×
        for j in 0..wc.edits.len() {
131216×
            if !edits_sel[j] {
13139×
                continue;
13147×
            }
13157×
            let e = &wc.edits[j];
13167×
            if let Some(&a) = add_index.get(&e.node()) {
13175×
                if !ops_sel[a] {
13181×
                    ops_sel[a] = true;
13191×
                    changed = true;
13204×
                }
13212×
            }
1322
            if let Edit::Insert {
13237×
                node,
13247×
                after,
13257×
                before,
1326
                ..
13277×
            } = e
1328
            {
132914×
                for r in [after, before] {
133014×
                    if let AtomRef::Atom { change, index } = r {
13312×
                        if change.is_zero() {
13320×
                            if let Some(k) = insert_for(node, *index) {
13330×
                                if !edits_sel[k] {
13340×
                                    edits_sel[k] = true;
13350×
                                    changed = true;
13360×
                                }
13370×
                            }
13382×
                        }
133912×
                    }
1340
                }
13410×
            }
1342
        }
1343
    }
1344
    // Renumber self-references: old atom index → new index among selected inserts.
13455×
    let mut remap: HashMap<(NodeId, u32), u32> = HashMap::new();
13465×
    let mut new_counts: HashMap<NodeId, u32> = HashMap::new();
134710×
    for (j, r) in insert_ranges.iter().enumerate() {
134810×
        if let (true, Some((node, a, b))) = (edits_sel[j], r) {
13494×
            let next = new_counts.entry(*node).or_default();
13504×
            for old in *a..*b {
13514×
                remap.insert((*node, old), *next);
13524×
                *next += 1;
13534×
            }
13546×
        }
1355
    }
13568×
    let map_ref = |r: AtomRef, node: &NodeId| -> AtomRef {
13572×
        match r {
13582×
            AtomRef::Atom { change, index } if change.is_zero() => AtomRef::Atom {
13590×
                change,
13600×
                index: remap.get(&(*node, index)).copied().unwrap_or(index),
13610×
            },
13628×
            other => other,
1363
        }
13648×
    };
13655×
    let ops = wc
13665×
        .tree_ops
13675×
        .iter()
13685×
        .enumerate()
13698×
        .filter(|(i, _)| ops_sel[*i])
13705×
        .map(|(_, o)| o.clone())
13715×
        .collect();
13725×
    let edits = wc
13735×
        .edits
13745×
        .iter()
13755×
        .enumerate()
137610×
        .filter(|(j, _)| edits_sel[*j])
13775×
        .map(|(_, e)| match e {
1378
            Edit::Insert {
13794×
                node,
13804×
                after,
13814×
                before,
13824×
                lines,
13834×
            } => Edit::Insert {
13844×
                node: *node,
13854×
                after: map_ref(*after, node),
13864×
                before: map_ref(*before, node),
13874×
                lines: lines.clone(),
13884×
            },
13890×
            other => other.clone(),
13904×
        })
13915×
        .collect();
13925×
    (ops, edits)
13935×
}
1394
1395
/// Render each tree operation and edit of `change` on its own, numbered.
13967×
pub fn render_hunks(change: &Change, base: &State, base_snapshot: &Snapshot) -> Vec<Hunk> {
13977×
    let by_node = base_snapshot.tree.by_node();
13987×
    let mut new_names: HashMap<NodeId, (NodeId, String, NodeKind)> = HashMap::new();
13998×
    for op in &change.tree_ops {
1400
        if let TreeOp::AddNode {
14018×
            node,
14028×
            parent,
14038×
            name,
14048×
            kind,
1405
            ..
14068×
        } = op
14078×
        {
14088×
            new_names.insert(*node, (*parent, name.clone(), *kind));
14098×
        }
1410
    }
14117×
    let mut out = Vec::new();
14128×
    for (i, op) in change.tree_ops.iter().enumerate() {
14138×
        let one = Change {
14148×
            tree_ops: vec![op.clone()],
14158×
            edits: vec![],
14168×
            ..change.clone()
14178×
        };
14188×
        let path = path_of_node(&op.node(), &by_node, &new_names).unwrap_or_default();
14198×
        out.push(Hunk {
14208×
            index: i,
14218×
            path,
14228×
            text: render_change(&one, base, base_snapshot),
14238×
        });
14248×
    }
142513×
    for (j, e) in change.edits.iter().enumerate() {
142613×
        let one = Change {
142713×
            tree_ops: vec![],
142813×
            edits: vec![e.clone()],
142913×
            ..change.clone()
143013×
        };
143113×
        let path = path_of_node(&e.node(), &by_node, &new_names).unwrap_or_default();
143213×
        out.push(Hunk {
143313×
            index: change.tree_ops.len() + j,
143413×
            path,
143513×
            text: render_change(&one, base, base_snapshot),
143613×
        });
143713×
    }
14387×
    out
14397×
}
1440
1441
/// One file's part of a diff, for `oxy diff --json`.
1442
#[derive(Clone, Debug)]
1443
pub struct FileDiff {
1444
    pub path: String,
1445
    /// `added`, `removed`, `modified`, `renamed`, `mode`, `replaced`.
1446
    pub kind: String,
1447
    pub hunks: Vec<DiffHunk>,
1448
    /// For renames, the previous path.
1449
    pub from: Option<String>,
1450
}
1451
1452
/// Lines inserted after `base_line` (0 = before the first line) and lines
1453
/// removed, with their base line numbers.
1454
#[derive(Clone, Debug, Default)]
1455
pub struct DiffHunk {
1456
    pub base_line: u32,
1457
    pub added: Vec<Vec<u8>>,
1458
    pub removed: Vec<(u32, Vec<u8>)>,
1459
}
1460
1461
impl Repo {
1462
    /// The content of `path` in a state: `heads`, or the working copy's base.
146326×
    pub fn file_at(&self, path: &str, heads: Option<&[ChangeHash]>) -> Result<Vec<u8>> {
1464
        // The state's history and tree, then the one file's graph: nothing
1465
        // else is built or rendered (oxygen#92).
1466
        // The working copy's base, and a state named by its heads (`-r
1467
        // main` on main), come from the workspace's state index.
146826×
        let base = if self.is_bare() {
14695×
            None
1470
        } else {
147121×
            Some(self.working_copy()?.1.deps)
1472
        };
147326×
        let same = |a: &[ChangeHash], b: &[ChangeHash]| {
14741×
            let (mut a, mut b) = (a.to_vec(), b.to_vec());
14751×
            a.sort();
14761×
            b.sort();
14771×
            a == b
14781×
        };
147926×
        let mut state = match (heads, base) {
14801×
            (Some(h), Some(b)) if same(h, &b) => self.load_state_indexed(&b)?,
14815×
            (Some(h), _) => self.load_state_history(h)?,
148220×
            (None, Some(b)) => self.load_state_indexed(&b)?,
14830×
            (None, None) => return fail("this is a bare repository: it has no working copy"),
1484
        };
148526×
        let view = state.tree().view();
148626×
        let Some(node) = view.entries.get(path).map(|e| e.node) else {
14870×
            return fail(format!("{path} is not in that state"));
1488
        };
148926×
        self.load_graphs_of(&mut state, &[node])?;
149025×
        let snapshot = state.materialize_paths(view, [path]);
149125×
        let Some(f) = snapshot.files.get(path) else {
14920×
            return fail(format!("{path} is not in that state"));
1493
        };
149425×
        match &f.content {
14950×
            FileContent::Dir => fail(format!("{path} is a directory")),
149625×
            FileContent::Text { bytes, .. } => Ok(bytes.clone()),
14970×
            FileContent::Opaque { blobs } => match blobs.first() {
14980×
                Some(b) => self.read_blob(b),
14990×
                None => Ok(vec![]),
1500
            },
1501
        }
150226×
    }
1503
1504
    /// Restore `paths` (files or directories) to the working copy's base:
1505
    /// edits are undone, additions removed, removals put back. Returns the
1506
    /// number of paths touched.
15072×
    pub fn discard(&self, paths: &[String]) -> Result<usize> {
15082×
        let _wc = self.lock_working_copy()?;
15092×
        let snap = self.snapshot_status("oxy discard")?;
15107×
        let under = |p: &str| {
15117×
            paths
15127×
                .iter()
15137×
                .any(|x| p == x || p.starts_with(&format!("{}/", x.trim_end_matches('/'))))
15147×
        };
15152×
        let attrs = Repo::state_attributes(&snap.base_snapshot);
15162×
        let mut n = 0;
15173×
        for s in &snap.statuses {
15183×
            if !under(&s.path) {
15191×
                continue;
15202×
            }
15212×
            match snap.base_snapshot.files.get(&s.path) {
1522
                None => {
1523
                    // Added (or the new name of a rename): remove it.
15241×
                    let full = self.root.join(&s.path);
15251×
                    if full.is_dir() {
15260×
                        let _ = fs::remove_dir_all(&full);
15271×
                    } else {
15281×
                        let _ = fs::remove_file(&full);
15291×
                    }
15301×
                    n += 1;
1531
                }
15321×
                Some(f) => {
15331×
                    match &f.content {
15340×
                        FileContent::Dir => fs::create_dir_all(self.root.join(&s.path))?,
15351×
                        FileContent::Text { bytes, .. } => {
15361×
                            let on_disk: std::borrow::Cow<[u8]> = if attrs.lookup(&s.path).eol
15371×
                                == Some(crate::attributes::Eol::Crlf)
1538
                            {
15390×
                                crate::attributes::to_crlf(bytes).into()
1540
                            } else {
15411×
                                bytes.as_slice().into()
1542
                            };
15431×
                            self.write_file(&s.path, &on_disk, f.mode.exec)?;
1544
                        }
15450×
                        FileContent::Opaque { blobs } => {
15460×
                            let bytes = match blobs.first() {
15470×
                                Some(b) => self.read_blob(b)?,
15480×
                                None => vec![],
1549
                            };
15500×
                            if f.kind == NodeKind::Symlink {
15510×
                                self.write_symlink(&s.path, &bytes)?;
15520×
                            } else if f.kind == NodeKind::File {
15530×
                                self.write_file(&s.path, &bytes, f.mode.exec)?;
15540×
                            }
1555
                        }
1556
                    }
15571×
                    n += 1;
1558
                }
1559
            }
1560
        }
1561
        // A rename's old name is not in the statuses; put it back too.
15624×
        for (p, f) in &snap.base_snapshot.files {
15634×
            if under(p)
15641×
                && !self.root.join(p).exists()
15650×
                && f.kind != NodeKind::Dir
15660×
                && f.kind != NodeKind::Subrepo
15670×
                && !snap.statuses.iter().any(|s| s.path == *p)
1568
            {
15690×
                let bytes = match &f.content {
15700×
                    FileContent::Text { bytes, .. } => bytes.clone(),
15710×
                    FileContent::Opaque { blobs } => blobs
15720×
                        .first()
15730×
                        .map(|b| self.read_blob(b))
15740×
                        .transpose()?
15750×
                        .unwrap_or_default(),
15760×
                    FileContent::Dir => continue,
1577
                };
15780×
                if f.kind == NodeKind::Symlink {
15790×
                    self.write_symlink(p, &bytes)?
1580
                } else {
15810×
                    self.write_file(p, &bytes, f.mode.exec)?
1582
                }
15830×
                n += 1;
15844×
            }
1585
        }
15862×
        self.drop_manifest();
15872×
        Ok(n)
15882×
    }
1589
1590
    /// Undo the working-copy hunks `indices` (as `hunks` numbers them) on
1591
    /// disk, keeping the others: the files are rewritten from the base plus
1592
    /// the remaining hunks.
15931×
    pub fn discard_hunks(&self, indices: &[usize]) -> Result<usize> {
15941×
        let _wc = self.lock_working_copy()?;
15951×
        let (snap, hunks) = self.hunks()?;
15961×
        let keep: Vec<usize> = hunks
15971×
            .iter()
15981×
            .map(|h| h.index)
15991×
            .filter(|i| !indices.contains(i))
16001×
            .collect();
16011×
        let (ops, edits) = select_hunks(&snap.wc, &keep);
16021×
        let dropped_nodes: BTreeSet<NodeId> = hunks
16031×
            .iter()
16041×
            .filter(|h| indices.contains(&h.index))
16051×
            .map(|h| {
16061×
                if h.index < snap.wc.tree_ops.len() {
16070×
                    snap.wc.tree_ops[h.index].node()
1608
                } else {
16091×
                    snap.wc.edits[h.index - snap.wc.tree_ops.len()].node()
1610
                }
16111×
            })
16121×
            .collect();
16131×
        let mut partial = snap.wc.clone();
16141×
        partial.tree_ops = ops;
16151×
        partial.edits = edits;
16161×
        partial.normalize();
16171×
        let mut st = snap.base.clone();
16181×
        st.add_unchecked(partial);
1619
        // Every file of the target is rendered: build the graphs the
1620
        // partial snapshot left out.
16211×
        self.load_all_graphs(&mut st)?;
16221×
        let target = st.materialize();
16231×
        let by_node_target = target.tree.by_node();
16241×
        let by_node_base = snap.base_snapshot.tree.by_node();
16251×
        let attrs = Repo::state_attributes(&target);
16261×
        let mut n = 0;
16271×
        for node in dropped_nodes {
16281×
            match by_node_target.get(&node) {
16291×
                Some(e) => {
16301×
                    let f = &target.files[&e.path];
16311×
                    match &f.content {
16320×
                        FileContent::Dir => {}
16331×
                        FileContent::Text { bytes, .. } => {
16341×
                            let on_disk: std::borrow::Cow<[u8]> = if attrs.lookup(&e.path).eol
16351×
                                == Some(crate::attributes::Eol::Crlf)
1636
                            {
16370×
                                crate::attributes::to_crlf(bytes).into()
1638
                            } else {
16391×
                                bytes.as_slice().into()
1640
                            };
16411×
                            self.write_file(&e.path, &on_disk, f.mode.exec)?;
1642
                        }
16430×
                        FileContent::Opaque { blobs } => {
16440×
                            let bytes = blobs
16450×
                                .first()
16460×
                                .map(|b| self.read_blob(b))
16470×
                                .transpose()?
16480×
                                .unwrap_or_default();
16490×
                            if f.kind == NodeKind::Symlink {
16500×
                                self.write_symlink(&e.path, &bytes)?
1651
                            } else {
16520×
                                self.write_file(&e.path, &bytes, f.mode.exec)?
1653
                            }
1654
                        }
1655
                    }
1656
                    // A discarded rename: the new name goes.
16571×
                    if let Some(old_path) = snap
16581×
                        .statuses
16591×
                        .iter()
16601×
                        .find(|s| {
16611×
                            s.status == ChangeStatus::Renamed && !target.files.contains_key(&s.path)
16621×
                        })
16631×
                        .map(|s| s.path.clone())
16640×
                    {
16650×
                        let _ = fs::remove_file(self.root.join(old_path));
16661×
                    }
16671×
                    n += 1;
1668
                }
1669
                None => {
1670
                    // The node exists in neither target nor base with a live path: an addition to remove.
16710×
                    if let Some(added) = snap.statuses.iter().find(|s| {
16720×
                        s.status == ChangeStatus::Added
16730×
                            && !target.files.contains_key(&s.path)
16740×
                            && !by_node_base.contains_key(&node)
16750×
                    }) {
16760×
                        let full = self.root.join(&added.path);
16770×
                        if full.is_dir() {
16780×
                            let _ = fs::remove_dir_all(&full);
16790×
                        } else {
16800×
                            let _ = fs::remove_file(&full);
16810×
                        }
16820×
                        n += 1;
16830×
                    }
1684
                }
1685
            }
1686
        }
16871×
        self.drop_manifest();
16881×
        Ok(n)
16891×
    }
1690
1691
    /// The diff of the working copy (`None`) or of a change, per file with
1692
    /// line-numbered hunks (spec: `oxy diff --json`).
16936×
    pub fn diff_files(&self, change: Option<&ChangeHash>) -> Result<Vec<FileDiff>> {
16946×
        let (c, base, base_snapshot) = match change {
16952×
            Some(h) => {
16962×
                let c = self.store.get_change(h)?;
16972×
                let (base, snapshot) = self.change_base(&c)?;
16982×
                (c, base, snapshot)
1699
            }
17004×
            None => match self.working_copy_with_base_unless_unchanged()? {
17013×
                Some(all) => all,
17021×
                None => return Ok(Vec::new()),
1703
            },
1704
        };
17055×
        Ok(diff_files_of(&c, &base, &base_snapshot))
17066×
    }
1707
1708
    /// The snapshot a command would take, without taking it, when the
1709
    /// status cache holds: every file is as the last snapshot left it, so
1710
    /// that snapshot's working-copy change is the answer. Its base comes
1711
    /// from the state index, the `.oxyattributes` files from disk. `None`
1712
    /// when a snapshot must be taken, and when there is nothing to commit
1713
    /// (the snapshot then says so). A conflicted base never gets here: the
1714
    /// cache does not hold for one.
1715685×
    pub(crate) fn snapshot_from_cache(&self) -> Result<Option<SnapshotResult>> {
1716685×
        let Some(statuses) = self.status_by_manifest()? else {
1717463×
            return Ok(None);
1718
        };
1719222×
        if statuses.is_empty() {
17202×
            return Ok(None);
1721220×
        }
1722220×
        let _t = crate::timing::phase("snapshot: from the status cache");
1723220×
        let (wc_hash, wc) = self.working_copy()?;
1724220×
        let base = self.load_state_indexed(&wc.deps)?;
1725220×
        let (base, base_snapshot) = self.render_base_for(&wc, base)?;
1726220×
        let attribute_files: Vec<(String, Vec<u8>)> = match self.read_manifest()? {
1727220×
            Some((_, _, m)) => m
1728220×
                .entries
1729220×
                .keys()
1730689×
                .filter(|p| is_attributes(p))
1731220×
                .filter_map(|p| fs::read(self.root.join(p)).ok().map(|b| (p.clone(), b)))
1732220×
                .collect(),
17330×
            None => Vec::new(),
1734
        };
1735220×
        let attributes = crate::attributes::Attributes::from_files(
1736220×
            attribute_files
1737220×
                .iter()
1738220×
                .map(|(p, b)| (p.as_str(), b.as_slice())),
1739
        );
1740220×
        Ok(Some(SnapshotResult {
1741220×
            wc_hash,
1742220×
            wc,
1743220×
            base,
1744220×
            base_snapshot,
1745220×
            statuses,
1746220×
            changed: false,
1747220×
            resolves: false,
1748220×
            contributors: Vec::new(),
1749220×
            attributes,
1750220×
            untracked_nested: self.untracked_nested.borrow().clone(),
1751220×
            manifest: None,
1752220×
        }))
1753685×
    }
1754
1755
    /// The working-copy change with its base state and a snapshot of the
1756
    /// base, for showing what the working copy changes ([`render_change`],
1757
    /// [`diff_files_of`]). While the status cache holds (every file as the
1758
    /// last snapshot left it), the stored working-copy change is the
1759
    /// answer, no snapshot is taken, and the base's snapshot holds its tree
1760
    /// but no file; otherwise a snapshot makes all three.
17612×
    pub fn working_copy_with_base(&self) -> Result<(Change, State, Snapshot)> {
17622×
        match self.working_copy_with_base_unless_unchanged()? {
17631×
            Some(all) => Ok(all),
1764
            None => {
17651×
                let (_, wc) = self.working_copy()?;
17661×
                let base = self.load_state_indexed(&wc.deps)?;
17671×
                let (base, snapshot) = self.tree_base_for(&wc, base)?;
17681×
                Ok((wc, base, snapshot))
1769
            }
1770
        }
17712×
    }
1772
1773
    /// [`Repo::working_copy_with_base`], or `None` when the status cache
1774
    /// holds and the working-copy change changes nothing: its diff is
1775
    /// empty, and the base is not loaded for it (oxygen#94).
17767×
    pub fn working_copy_with_base_unless_unchanged(
17777×
        &self,
17787×
    ) -> Result<Option<(Change, State, Snapshot)>> {
17797×
        if self.status_by_manifest()?.is_some() {
17805×
            let (_, wc) = self.working_copy()?;
17815×
            if wc.tree_ops.is_empty() && wc.edits.is_empty() {
17823×
                return Ok(None);
17832×
            }
17842×
            let base = self.load_state_indexed(&wc.deps)?;
17852×
            let (base, snapshot) = self.tree_base_for(&wc, base)?;
17862×
            return Ok(Some((wc, base, snapshot)));
17872×
        }
17882×
        let snap = self.snapshot_status("oxy diff")?;
17892×
        Ok(Some((snap.wc, snap.base, snap.base_snapshot)))
17907×
    }
1791
1792
    /// The log restricted to changes touching `path` (its node, so renames
1793
    /// are followed), on the current bookmark.
17945×
    pub fn log_for_path(&self, path: &str) -> Result<Vec<LogEntry>> {
17955×
        let heads = self.bookmark_heads()?;
17965×
        if self.is_bare() {
17971×
            return self.log_for_path_in(path, &heads);
17984×
        }
17994×
        let entries = self.log()?;
18004×
        let (_, wc) = self.working_copy()?;
1801
        // The path's node from the trees alone: the base's (from the
1802
        // workspace's state index), else the bookmark's. Nothing is
1803
        // rendered.
18044×
        let node_in = |state: State| state.tree().view().entries.get(path).map(|e| e.node);
18054×
        let node = match node_in(self.load_state_indexed(&wc.deps)?) {
18064×
            Some(n) => Some(n),
18070×
            None => node_in(self.load_state_history(&heads)?),
1808
        };
18094×
        let Some(node) = node else {
18100×
            return fail(format!("{path} is not tracked"));
1811
        };
18124×
        Ok(self.entries_touching(entries, node))
18135×
    }
1814
1815
    /// The log of the state with these heads restricted to changes touching
1816
    /// `path` (its node, so renames are followed).
18172×
    pub fn log_for_path_in(&self, path: &str, heads: &[ChangeHash]) -> Result<Vec<LogEntry>> {
18182×
        self.log_for_path_of(&self.load_state_history(heads)?, path, None)
18192×
    }
1820
1821
    /// The changes of a loaded state touching `path`, newest first, at most
1822
    /// `limit` of them. The path is found in the state's tree alone, so no
1823
    /// file is rendered, and the walk stops once `limit` are found.
18245×
    pub fn log_for_path_of(
18255×
        &self,
18265×
        state: &State,
18275×
        path: &str,
18285×
        limit: Option<usize>,
18295×
    ) -> Result<Vec<LogEntry>> {
18305×
        let Some(node) = state.tree().view().entries.get(path).map(|e| e.node) else {
18311×
            return fail(format!("{path} is not in that state"));
1832
        };
18334×
        Ok(self.entries_touching_up_to(self.log_of(state), node, limit))
18345×
    }
1835
18364×
    fn entries_touching(&self, entries: Vec<LogEntry>, node: NodeId) -> Vec<LogEntry> {
18374×
        self.entries_touching_up_to(entries, node, None)
18384×
    }
1839
18408×
    fn entries_touching_up_to(
18418×
        &self,
18428×
        entries: Vec<LogEntry>,
18438×
        node: NodeId,
18448×
        limit: Option<usize>,
18458×
    ) -> Vec<LogEntry> {
18468×
        let mut touched = self.touched();
18478×
        if limit.is_none() {
18487×
            let hashes: Vec<ChangeHash> = entries.iter().map(|e| e.hash).collect();
18497×
            touched.fill(self, &hashes);
18501×
        }
18518×
        let wanted = HashSet::from([node]);
18528×
        let mut out = Vec::new();
185328×
        for e in entries {
185428×
            if limit.is_some_and(|n| out.len() >= n) {
18551×
                break;
185627×
            }
185727×
            if touched.touches(self, &e.hash, &wanted) {
185818×
                out.push(e);
185918×
            }
1860
        }
18618×
        touched.save();
18628×
        out
18638×
    }
1864
}
1865
1866
/// Per-file hunks of `change` against `base`.
18675×
pub fn diff_files_of(change: &Change, base: &State, base_snapshot: &Snapshot) -> Vec<FileDiff> {
18685×
    let by_node = base_snapshot.tree.by_node();
18695×
    let mut new_names: HashMap<NodeId, (NodeId, String, NodeKind)> = HashMap::new();
18705×
    for op in &change.tree_ops {
1871
        if let TreeOp::AddNode {
18722×
            node,
18732×
            parent,
18742×
            name,
18752×
            kind,
1876
            ..
18773×
        } = op
18782×
        {
18792×
            new_names.insert(*node, (*parent, name.clone(), *kind));
18802×
        }
1881
    }
18829×
    let path_of = |n: &NodeId| -> String {
18839×
        path_of_node(n, &by_node, &new_names).unwrap_or_else(|| format!("<node {}>", n.short(8)))
18849×
    };
18855×
    let mut files: BTreeMap<NodeId, FileDiff> = BTreeMap::new();
18865×
    let mut order: Vec<NodeId> = Vec::new();
188714×
    let mut entry = |files: &mut BTreeMap<NodeId, FileDiff>, n: NodeId, kind: &str| {
188814×
        if let std::collections::btree_map::Entry::Vacant(v) = files.entry(n) {
18899×
            order.push(n);
18909×
            v.insert(FileDiff {
18919×
                path: path_of(&n),
18929×
                kind: kind.to_string(),
18939×
                hunks: vec![],
18949×
                from: None,
18959×
            });
18969×
        }
189714×
    };
18985×
    for op in &change.tree_ops {
18993×
        match op {
19002×
            TreeOp::AddNode { node, kind, .. } => {
19012×
                if *kind != NodeKind::Dir {
19022×
                    entry(&mut files, *node, "added");
19032×
                }
1904
            }
19051×
            TreeOp::RemoveNode { node, .. } => {
19061×
                entry(&mut files, *node, "removed");
19071×
                if let Some(e) = by_node.get(node) {
19081×
                    files.get_mut(node).unwrap().path = e.path.clone();
19091×
                }
1910
            }
1911
            TreeOp::Move {
19120×
                node, parent, name, ..
1913
            } => {
19140×
                entry(&mut files, *node, "renamed");
19150×
                let f = files.get_mut(node).unwrap();
19160×
                f.from = by_node.get(node).map(|e| e.path.clone());
19170×
                let dir = if parent.is_root() {
19180×
                    String::new()
1919
                } else {
19200×
                    path_of_node(parent, &by_node, &new_names).unwrap_or_default()
1921
                };
19220×
                f.path = if dir.is_empty() {
19230×
                    name.clone()
1924
                } else {
19250×
                    format!("{dir}/{name}")
1926
                };
1927
            }
19280×
            TreeOp::SetMode { node, .. } => entry(&mut files, *node, "mode"),
1929
        }
1930
    }
1931
    // Base line numbers per node, 1-based.
19325×
    let mut line_of: HashMap<NodeId, HashMap<(ChangeHash, u32), u32>> = HashMap::new();
193311×
    let mut lines_for = |n: &NodeId| -> HashMap<(ChangeHash, u32), u32> {
193411×
        line_of
193511×
            .entry(*n)
193611×
            .or_insert_with(|| {
19378×
                base.live_lines(n)
19388×
                    .into_iter()
19398×
                    .enumerate()
194011×
                    .map(|(i, (k, _))| (k, i as u32 + 1))
19418×
                    .collect()
19428×
            })
194311×
            .clone()
194411×
    };
194511×
    for e in &change.edits {
194611×
        let n = e.node();
194711×
        let is_new = new_names.contains_key(&n);
194811×
        entry(&mut files, n, if is_new { "added" } else { "modified" });
194911×
        let f = files.get_mut(&n).unwrap();
195011×
        match e {
19519×
            Edit::Insert { after, lines, .. } => {
19529×
                let map = lines_for(&n);
19539×
                let base_line = match after {
19543×
                    AtomRef::Start => 0,
19550×
                    AtomRef::End => map.len() as u32,
19566×
                    AtomRef::Atom { change, index } if change.is_zero() => {
19570×
                        f.hunks.last().map(|h| h.base_line).unwrap_or(0)
1958
                    }
19596×
                    AtomRef::Atom { change, index } => {
19606×
                        map.get(&(*change, *index)).copied().unwrap_or(0)
1961
                    }
1962
                };
19639×
                f.hunks.push(DiffHunk {
19649×
                    base_line,
19659×
                    added: lines.to_vec(),
19669×
                    removed: vec![],
19679×
                });
1968
            }
19692×
            Edit::Delete { atoms, .. } => {
19702×
                let map = lines_for(&n);
19712×
                let mut removed = Vec::new();
19722×
                for r in atoms {
19732×
                    for i in r.from..r.to {
19742×
                        let line = map.get(&(r.change, i)).copied().unwrap_or(0);
19752×
                        let text = base
19762×
                            .file(&n)
19772×
                            .and_then(|g| g.content(&(r.change, i)))
19782×
                            .unwrap_or(b"")
19792×
                            .to_vec();
19802×
                        removed.push((line, text));
1981
                    }
1982
                }
19832×
                let base_line = removed
19842×
                    .first()
19852×
                    .map(|(l, _)| l.saturating_sub(1))
19862×
                    .unwrap_or(0);
19872×
                f.hunks.push(DiffHunk {
19882×
                    base_line,
19892×
                    added: vec![],
19902×
                    removed,
19912×
                });
1992
            }
1993
            Edit::Replace { .. } => {
19940×
                if !is_new {
19950×
                    f.kind = "replaced".into();
19960×
                }
1997
            }
1998
        }
1999
    }
20009×
    order.into_iter().filter_map(|n| files.remove(&n)).collect()
20015×
}
2002
2003
/// Builds the tree ops and edits of a working-copy change against a base.
2004
/// `BLAKE3("oxy node 1" 0 seed kind path)`, truncated: a node identifier
2005
/// that the same seed and path always give (never the root's).
200659×
fn seeded_node(seed: &[u8; 32], path: &str, kind: NodeKind) -> NodeId {
200759×
    let mut h = blake3::Hasher::new();
200859×
    h.update(b"oxy node 1\0");
200959×
    h.update(seed);
201059×
    h.update(&[match kind {
201145×
        NodeKind::File => 1,
201213×
        NodeKind::Dir => 2,
20130×
        NodeKind::Symlink => 3,
20141×
        NodeKind::Subrepo => 4,
2015
    }]);
201659×
    h.update(path.as_bytes());
201759×
    let mut id = [0u8; 16];
201859×
    id.copy_from_slice(&h.finalize().as_bytes()[..16]);
201959×
    if id == [0; 16] {
20200×
        id[15] = 1;
202159×
    }
202259×
    NodeId(id)
202359×
}
2024
2025
struct ChangeBuilder<'a> {
2026
    base: &'a State,
2027
    base_snapshot: &'a Snapshot,
2028
    tree_ops: Vec<TreeOp>,
2029
    edits: Vec<Edit>,
2030
    statuses: Vec<PathStatus>,
2031
    dir_nodes: HashMap<String, NodeId>,
2032
    reuse: HashMap<String, (NodeId, NodeKind)>,
2033
    /// Changes that contributed to conflicts this working copy resolves.
2034
    contributors: BTreeSet<ChangeHash>,
2035
    resolves: bool,
2036
    attrs: crate::attributes::Attributes,
2037
    blobs: HashMap<String, ObjectRef>,
2038
    /// Opaque files whose replace conflict the content settles.
2039
    resolve: HashSet<String>,
2040
    /// When set, new nodes' identifiers derive from it and their paths.
2041
    node_seed: Option<[u8; 32]>,
2042
    /// For a Git import, the base's dead directories by where they were
2043
    /// (see `ensure_dir`), worked out when first needed.
2044
    dead_dirs: Option<HashMap<(NodeId, String), (NodeId, crate::tree::OpKey)>>,
2045
    /// Text edits of edited files, worked out in parallel beforehand
2046
    /// (`diff_in_parallel`); a file not here is diffed when it is updated.
2047
    diffs: HashMap<String, Vec<Edit>>,
2048
}
2049
2050
impl<'a> ChangeBuilder<'a> {
20512050×
    fn new(
20522050×
        base: &'a State,
20532050×
        base_snapshot: &'a Snapshot,
20542050×
        reuse: HashMap<String, (NodeId, NodeKind)>,
20552050×
    ) -> Self {
20562050×
        let mut dir_nodes: HashMap<String, NodeId> = HashMap::new();
20572050×
        dir_nodes.insert(String::new(), NodeId::ROOT);
205817815×
        for (p, e) in base_snapshot.tree.entries.iter() {
205917815×
            if e.kind == NodeKind::Dir {
2060560×
                dir_nodes.insert(p.clone(), e.node);
206117255×
            }
2062
        }
20632050×
        ChangeBuilder {
20642050×
            base,
20652050×
            base_snapshot,
20662050×
            tree_ops: vec![],
20672050×
            edits: vec![],
20682050×
            statuses: vec![],
20692050×
            dir_nodes,
20702050×
            reuse,
20712050×
            contributors: BTreeSet::new(),
20722050×
            resolves: false,
20732050×
            attrs: crate::attributes::Attributes::default(),
20742050×
            blobs: HashMap::new(),
20752050×
            resolve: HashSet::new(),
20762050×
            node_seed: None,
20772050×
            dead_dirs: None,
20782050×
            diffs: HashMap::new(),
20792050×
        }
20802050×
    }
2081
2082
    /// Diff the edited text files among `entries` against their base
2083
    /// renderings at once, on every core, for `update_path` to take: the
2084
    /// same diff it would make, only not one file after another.
20852050×
    fn diff_in_parallel(&mut self, entries: &BTreeMap<String, FsEntry>) {
2086
        use rayon::prelude::*;
20872050×
        let files = &self.base_snapshot.files;
20882050×
        let edited: Vec<(&String, &FsEntry, &crate::state::FileState)> = entries
20892050×
            .iter()
209018859×
            .filter_map(|(p, e)| {
209118859×
                let x = files.get(p)?;
209212785×
                (x.kind == e.kind && !e.unchanged && !self.opaque_for(p, e)).then_some((p, e, x))
209318859×
            })
20942050×
            .collect();
2095
        // Below this, starting the threads costs more than it saves.
20962050×
        if edited.len() < 16 {
20972040×
            return;
209810×
        }
209910×
        let this = &*self;
210010×
        let attrs = &this.attrs;
210110×
        let diffs: Vec<(String, Vec<Edit>)> = edited
210210×
            .par_iter()
21035564×
            .filter_map(|(p, e, x)| {
2104
                let FileContent::Text {
21055564×
                    bytes,
21065564×
                    materialized,
21075564×
                } = &x.content
2108
                else {
21090×
                    return None;
2110
                };
21115564×
                if materialized.is_conflicted() {
21120×
                    return None;
21135564×
                }
21145564×
                let disk_text = if attrs.lookup(p).eol.is_some() {
21150×
                    std::sync::Arc::new(crate::attributes::normalize_eol(&e.content))
2116
                } else {
21175564×
                    e.content.clone()
2118
                };
21195564×
                if *bytes == *disk_text {
21205484×
                    return None;
212180×
                }
212280×
                let keys = materialized.live_atoms();
212380×
                let lines = crate::diff::split_lines(bytes);
212480×
                (keys.len() == lines.len()).then(|| {
212580×
                    let edits = crate::diff::text_edits_of(x.node, &keys, &lines, &disk_text);
212680×
                    ((*p).clone(), edits)
212780×
                })
21285564×
            })
212910×
            .collect();
213010×
        self.diffs = diffs.into_iter().collect();
21312050×
    }
2132
21332050×
    fn with_attributes(mut self, attrs: crate::attributes::Attributes) -> Self {
21342050×
        self.attrs = attrs;
21352050×
        self
21362050×
    }
2137
2138
    /// Whether a file is opaque: forced by `binary`, forced text by `text`
2139
    /// only when it has no NUL bytes, else sniffed.
2140
    /// The blob for an opaque entry: the cached one, else stored now.
2141132×
    fn blob_of(
2142132×
        &mut self,
2143132×
        repo: &Repo,
2144132×
        path: &str,
2145132×
        e: &FsEntry,
2146132×
        nocompress: bool,
2147132×
    ) -> Result<ObjectRef> {
2148132×
        let b = match e.blob {
21490×
            Some(b) => b,
2150132×
            None => repo.store_blob_for(&e.content, nocompress)?,
2151
        };
2152132×
        self.blobs.insert(path.to_string(), b);
2153132×
        Ok(b)
2154132×
    }
2155
215623157×
    fn opaque_for(&self, path: &str, e: &FsEntry) -> bool {
215723157×
        if e.blob.is_some() {
21588×
            return true;
215923149×
        }
216023149×
        if e.kind == NodeKind::Symlink || e.kind == NodeKind::Subrepo {
216179×
            return true;
216223070×
        }
216323070×
        match self.attrs.lookup(path).binary {
216412×
            Some(true) => true,
21650×
            Some(false) => is_binary(&e.content),
216623058×
            None => is_binary(&e.content),
2167
        }
216823157×
    }
2169
2170
    /// Text content as stored: `\r\n` normalized for `eol=` files.
217114487×
    fn stored_text(
217214487×
        &self,
217314487×
        path: &str,
217414487×
        content: &std::sync::Arc<Vec<u8>>,
217514487×
    ) -> std::sync::Arc<Vec<u8>> {
217614487×
        if self.attrs.lookup(path).eol.is_some() {
21777×
            std::sync::Arc::new(crate::attributes::normalize_eol(content))
2178
        } else {
217914480×
            content.clone()
2180
        }
218114487×
    }
2182
2183
    /// The identifier of a node added at `path`: reused when asked, derived
2184
    /// from the seed when there is one, random otherwise.
21856195×
    fn new_node(&self, path: &str, kind: NodeKind) -> NodeId {
21866195×
        if let Some(n) = self.reused_node(path, kind) {
2187149×
            return n;
21886046×
        }
21896046×
        match &self.node_seed {
219059×
            Some(seed) => seeded_node(seed, path, kind),
21915987×
            None => NodeId::random(),
2192
        }
21936195×
    }
2194
21956195×
    fn reused_node(&self, path: &str, kind: NodeKind) -> Option<NodeId> {
21966195×
        self.reuse
21976195×
            .get(path)
21986195×
            .filter(|(_, k)| *k == kind)
21996195×
            .map(|(n, _)| *n)
22006195×
    }
2201
2202
    /// A dead directory last placed at (`parent`, `name`) in the base.
220315×
    fn revivable_dir(
220415×
        &mut self,
220515×
        parent: NodeId,
220615×
        name: &str,
220715×
    ) -> Option<(NodeId, crate::tree::OpKey)> {
220815×
        let base = self.base;
220915×
        self.dead_dirs
221015×
            .get_or_insert_with(|| base.tree().dead_dirs())
221115×
            .get(&(parent, name.to_string()))
221215×
            .copied()
221315×
    }
2214
22156075×
    fn ensure_dir(&mut self, path: &str) -> NodeId {
22166075×
        if let Some(n) = self.dir_nodes.get(path) {
22175926×
            return *n;
2218149×
        }
2219149×
        let mut parent = NodeId::ROOT;
2220149×
        let mut prefix = String::new();
2221168×
        for comp in path.split('/') {
2222168×
            if !prefix.is_empty() {
222319×
                prefix.push('/');
2224149×
            }
2225168×
            prefix.push_str(comp);
2226168×
            let known = self.dir_nodes.get(&prefix).copied();
2227168×
            let revive = match known {
222813×
                Some(_) => None,
2229155×
                None if self.node_seed.is_some() => self.revivable_dir(parent, comp),
2230140×
                None => None,
2231
            };
2232168×
            parent = match (known, revive) {
223313×
                (Some(n), _) => n,
22342×
                (None, Some((node, old))) => {
2235
                    // Git has no identity for a directory: one that comes
2236
                    // back where it was is the same directory, brought back
2237
                    // with a move, so lines of history that placed files in
2238
                    // it before and after agree (oxygen#30).
22392×
                    self.tree_ops.push(TreeOp::Move {
22402×
                        node,
22412×
                        old: OpRef {
22422×
                            change: old.0,
22432×
                            index: old.1,
22442×
                        },
22452×
                        parent,
22462×
                        name: comp.to_string(),
22472×
                    });
22482×
                    self.dir_nodes.insert(prefix.clone(), node);
22492×
                    node
2250
                }
2251
                (None, None) => {
2252153×
                    let node = self.new_node(&prefix, NodeKind::Dir);
2253153×
                    self.tree_ops.push(TreeOp::AddNode {
2254153×
                        node,
2255153×
                        kind: NodeKind::Dir,
2256153×
                        mode: Mode::default(),
2257153×
                        parent,
2258153×
                        name: comp.to_string(),
2259153×
                    });
2260153×
                    self.dir_nodes.insert(prefix.clone(), node);
2261153×
                    node
2262
                }
2263
            };
2264
        }
2265149×
        parent
22666075×
    }
2267
22686042×
    fn add_path(&mut self, repo: &Repo, path: &str, e: &FsEntry) -> Result<()> {
22696042×
        let (dir, name) = split_path(path);
22706042×
        let parent = self.ensure_dir(dir);
22716042×
        let node = self.new_node(path, e.kind);
22726042×
        let opaque = self.opaque_for(path, e);
22736042×
        let nocompress = self.attrs.lookup(path).nocompress;
22746042×
        self.tree_ops.push(TreeOp::AddNode {
22756042×
            node,
22766042×
            kind: e.kind,
22776042×
            mode: Mode {
22786042×
                exec: e.exec,
22796042×
                opaque,
22806042×
            },
22816042×
            parent,
22826042×
            name: name.to_string(),
22836042×
        });
22846042×
        if opaque {
228596×
            let blob = self.blob_of(repo, path, e, nocompress)?;
228696×
            self.edits.push(Edit::Replace {
228796×
                node,
228896×
                old: None,
228996×
                new: Some(blob),
229096×
            });
22915946×
        } else if !e.content.is_empty() {
22925913×
            let content = self.stored_text(path, &e.content);
22935913×
            self.edits.push(Edit::Insert {
22945913×
                node,
22955913×
                after: crate::objects::AtomRef::Start,
22965913×
                before: crate::objects::AtomRef::End,
22975913×
                lines: crate::lines::Lines::split_shared(content),
22985913×
            });
22995913×
        }
23006042×
        self.statuses.push(PathStatus {
23016042×
            path: path.to_string(),
23026042×
            status: ChangeStatus::Added,
23036042×
        });
23046042×
        Ok(())
23056042×
    }
2306
2307
    /// Move the node at `old` to `new` and apply `e`'s content to it.
230833×
    fn move_path(
230933×
        &mut self,
231033×
        repo: &Repo,
231133×
        old: &str,
231233×
        new: &str,
231333×
        e: &FsEntry,
231433×
        expected: &crate::state::FileState,
231533×
    ) -> Result<()> {
231633×
        let placement = self.base_snapshot.tree.entries[old].placement;
231733×
        let (dir, name) = split_path(new);
231833×
        let parent = self.ensure_dir(dir);
231933×
        self.tree_ops.push(TreeOp::Move {
232033×
            node: expected.node,
232133×
            old: op_ref(placement),
232233×
            parent,
232333×
            name: name.to_string(),
232433×
        });
232533×
        for k in self.same_place(&expected.node, placement) {
23260×
            self.tree_ops.push(TreeOp::RemoveNode {
23270×
                node: expected.node,
23280×
                placement: op_ref(k),
23290×
            });
23300×
        }
233133×
        self.update_path(repo, old, e, expected)?;
2332
        // update_path reports under the old path; the rename is what the user sees.
233333×
        self.statuses.retain(|s| s.path != old);
233433×
        self.statuses.push(PathStatus {
233533×
            path: new.to_string(),
233633×
            status: ChangeStatus::Renamed,
233733×
        });
233833×
        Ok(())
233933×
    }
2340
2341
    /// For a Git import: a node the commit keeps is in one place, the one
2342
    /// its tree names, so any other live placement it has in the base goes;
2343
    /// and a node the tree cannot show at all (an orphan) goes too. Such a
2344
    /// placement is out of sight, under a directory another line of history
2345
    /// removed, where no diff of the tree would ever reach it (oxygen#30).
234688×
    fn retire_stray_placements(&mut self) {
234788×
        let mut retired: HashSet<(ChangeHash, u32)> = HashSet::new();
234888×
        let mut placed: BTreeSet<NodeId> = BTreeSet::new();
234999×
        for op in &self.tree_ops {
235099×
            match op {
235119×
                TreeOp::Move { node, old, .. } => {
235219×
                    retired.insert((old.change, old.index));
235319×
                    placed.insert(*node);
235419×
                }
235521×
                TreeOp::RemoveNode { placement, .. } => {
235621×
                    retired.insert((placement.change, placement.index));
235721×
                }
235859×
                TreeOp::AddNode { .. } | TreeOp::SetMode { .. } => {}
2359
            }
2360
        }
2361
        // Nodes kept where they were: what the base's tree view shows and
2362
        // this change neither moves nor removes.
236388×
        let mut kept: HashMap<NodeId, (ChangeHash, u32)> = HashMap::new();
2364241×
        for e in self.base_snapshot.tree.entries.values() {
2365241×
            if !retired.contains(&e.placement) {
2366204×
                kept.insert(e.node, e.placement);
2367204×
            }
2368
        }
2369
        // Only a node with several live placements can have a stray one,
2370
        // and the tree view names each as a conflict.
237188×
        let multi: BTreeSet<NodeId> = self
237288×
            .base_snapshot
237388×
            .tree
237488×
            .conflicts
237588×
            .iter()
237688×
            .filter_map(|c| match c {
23771×
                crate::tree::TreeConflict::MultiPlacement { node, .. } => Some(*node),
23786×
                _ => None,
23797×
            })
238088×
            .collect();
238188×
        let mut extra = Vec::new();
2382
        // A node whose only placements are under a directory another line
2383
        // removed (an orphan) is nowhere the tree can show, and Git's tree
2384
        // has no such path: it goes.
238588×
        for c in &self.base_snapshot.tree.conflicts {
23867×
            let crate::tree::TreeConflict::Orphan { node, .. } = c else {
23876×
                continue;
2388
            };
23891×
            if kept.contains_key(node) || placed.contains(node) {
23900×
                continue;
23911×
            }
23921×
            for (k, _) in self.base.tree().live_placements(node) {
23931×
                if retired.insert(k) {
23941×
                    extra.push(TreeOp::RemoveNode {
23951×
                        node: *node,
23961×
                        placement: op_ref(k),
23971×
                    });
23981×
                }
2399
            }
2400
        }
240188×
        for node in placed
240288×
            .iter()
240388×
            .chain(multi.iter().filter(|n| kept.contains_key(*n)))
2404
        {
240519×
            for (k, _) in self.base.tree().live_placements(node) {
240618×
                if retired.contains(&k) || kept.get(node) == Some(&k) {
240717×
                    continue;
24081×
                }
24091×
                retired.insert(k);
24101×
                extra.push(TreeOp::RemoveNode {
24111×
                    node: *node,
24121×
                    placement: op_ref(k),
24131×
                });
2414
            }
2415
        }
241688×
        if !extra.is_empty() {
24172×
            self.resolves = true;
24182×
            self.tree_ops.extend(extra);
241986×
        }
242088×
    }
2421
2422
    /// The node's other live placements at the same place as `k`, the one
2423
    /// the tree shows: two lines of history that put a node back where it
2424
    /// was each leave a placement, and the tree shows them as one file. A
2425
    /// change that removes or moves it retires them together, or the copy
2426
    /// left behind outlives its directory (oxygen#88).
2427157×
    fn same_place(&self, node: &NodeId, k: (ChangeHash, u32)) -> Vec<(ChangeHash, u32)> {
2428157×
        let tree = self.base.tree();
2429157×
        let Some(shown) = tree.placement(&k) else {
24300×
            return Vec::new();
2431
        };
2432157×
        tree.live_placements(node)
2433157×
            .into_iter()
2434159×
            .filter(|(o, p)| *o != k && p.parent == shown.parent && p.name == shown.name)
2435157×
            .map(|(o, _)| o)
2436157×
            .collect()
2437157×
    }
2438
2439124×
    fn remove_path(&mut self, path: &str, expected: &crate::state::FileState) {
2440124×
        let entry = &self.base_snapshot.tree.entries[path];
2441124×
        let placement = entry.placement;
2442124×
        self.tree_ops.push(TreeOp::RemoveNode {
2443124×
            node: expected.node,
2444124×
            placement: op_ref(placement),
2445124×
        });
2446124×
        for k in self.same_place(&expected.node, placement) {
24471×
            self.tree_ops.push(TreeOp::RemoveNode {
24481×
                node: expected.node,
24491×
                placement: op_ref(k),
24501×
            });
24511×
        }
2452124×
        if path.contains(".oxy-conflict-") || expected.is_conflicted() {
24537×
            self.resolves = true;
2454117×
        }
2455124×
        self.statuses.push(PathStatus {
2456124×
            path: path.to_string(),
2457124×
            status: ChangeStatus::Removed,
2458124×
        });
2459124×
    }
2460
246112818×
    fn update_path(
246212818×
        &mut self,
246312818×
        repo: &Repo,
246412818×
        path: &str,
246512818×
        e: &FsEntry,
246612818×
        expected: &crate::state::FileState,
246712818×
    ) -> Result<()> {
246812818×
        if expected.kind != e.kind {
24691×
            self.remove_path(path, expected);
24701×
            self.statuses.pop();
24711×
            self.add_path(repo, path, e)?;
24721×
            self.statuses.pop();
24731×
            self.statuses.push(PathStatus {
24741×
                path: path.to_string(),
24751×
                status: ChangeStatus::Modified,
24761×
            });
24771×
            return Ok(());
247812817×
        }
247912817×
        let entry = &self.base_snapshot.tree.entries[path];
248012817×
        let node = expected.node;
248112817×
        let mode_atom = entry.mode_atom.map(op_ref);
248212817×
        if e.unchanged {
2483
            // Known equal to the base's unconflicted rendering (spec
2484
            // §12.3); only the executable bit can differ.
24854243×
            if e.exec != expected.mode.exec {
24863×
                if let Some(old) = mode_atom {
24873×
                    self.tree_ops.push(TreeOp::SetMode {
24883×
                        node,
24893×
                        old,
24903×
                        mode: Mode {
24913×
                            exec: e.exec,
24923×
                            opaque: false,
24933×
                        },
24943×
                    });
24953×
                    self.statuses.push(PathStatus {
24963×
                        path: path.to_string(),
24973×
                        status: ChangeStatus::ModeChanged,
24983×
                    });
24993×
                }
25004240×
            }
25014243×
            return Ok(());
25028574×
        }
25038574×
        let mut status: Option<ChangeStatus> = None;
25048574×
        let wants_opaque = self.opaque_for(path, e);
25058574×
        let nocompress = self.attrs.lookup(path).nocompress;
25068574×
        let disk_text = self.stored_text(path, &e.content);
25078574×
        match &expected.content {
25080×
            FileContent::Dir => {}
2509
            FileContent::Text {
25108350×
                bytes,
25118350×
                materialized,
2512
            } => {
25138350×
                if materialized.is_conflicted() {
251438×
                    if bytes == &*disk_text {
25156×
                        return Ok(());
251632×
                    }
251732×
                    if has_conflict_markers(&e.content) {
25184×
                        self.statuses.push(PathStatus {
25194×
                            path: path.to_string(),
25204×
                            status: ChangeStatus::ConflictEdited,
25214×
                        });
25224×
                        return Ok(());
252328×
                    }
2524
                    // Resolution (spec §9.6): diff against the unconflicted lines
2525
                    // with each region as a gap, then retire every region atom.
2526
                    // Whatever the author kept from a region is re-inserted as
2527
                    // new atoms, which leaves the graph totally ordered.
252828×
                    let regions = self.base.conflict_regions(&node);
252928×
                    let mut edits = text_edits(node, &regions.plain, &disk_text);
253028×
                    if !regions.region_atoms.is_empty() {
253128×
                        edits.insert(
253228×
                            0,
253328×
                            Edit::Delete {
253428×
                                node,
253528×
                                atoms: ranges(&regions.region_atoms),
253628×
                            },
253728×
                        );
253828×
                    }
253928×
                    self.edits.extend(edits);
254028×
                    self.contributors.extend(regions.contributors);
254128×
                    self.resolves = true;
254228×
                    if e.exec != expected.mode.exec {
25430×
                        if let Some(old) = mode_atom {
25440×
                            self.tree_ops.push(TreeOp::SetMode {
25450×
                                node,
25460×
                                old,
25470×
                                mode: Mode {
25480×
                                    exec: e.exec,
25490×
                                    opaque: false,
25500×
                                },
25510×
                            });
25520×
                        }
255328×
                    }
255428×
                    self.statuses.push(PathStatus {
255528×
                        path: path.to_string(),
255628×
                        status: ChangeStatus::Resolved,
255728×
                    });
255828×
                    return Ok(());
25598312×
                }
25608312×
                if wants_opaque {
2561
                    // Text → opaque (spec §7.5), by content or by attribute.
25622×
                    if let Some(old) = mode_atom {
25632×
                        self.tree_ops.push(TreeOp::SetMode {
25642×
                            node,
25652×
                            old,
25662×
                            mode: Mode {
25672×
                                exec: e.exec,
25682×
                                opaque: true,
25692×
                            },
25702×
                        });
25712×
                    }
25722×
                    let live: Vec<_> = repo_live_keys(self.base, &node);
25732×
                    if !live.is_empty() {
25742×
                        self.edits.push(Edit::Delete {
25752×
                            node,
25762×
                            atoms: ranges(&live),
25772×
                        });
25782×
                    }
25792×
                    let blob = self.blob_of(repo, path, e, nocompress)?;
25802×
                    self.edits.push(Edit::Replace {
25812×
                        node,
25822×
                        old: None,
25832×
                        new: Some(blob),
25842×
                    });
25852×
                    status = Some(ChangeStatus::Modified);
25868310×
                } else if bytes != &*disk_text {
2587813×
                    let mut laps = crate::timing::Laps::start();
2588
                    // The base's rendering holds its live atoms in order,
2589
                    // one per line (it is not conflicted here).
2590813×
                    let edits = match self.diffs.remove(path) {
259180×
                        Some(edits) => edits,
2592
                        None => {
2593733×
                            let keys = materialized.live_atoms();
2594733×
                            let lines = crate::diff::split_lines(bytes);
2595733×
                            if keys.len() == lines.len() {
2596733×
                                crate::diff::text_edits_of(node, &keys, &lines, &disk_text)
2597
                            } else {
25980×
                                text_edits(node, &self.base.live_lines(&node), &disk_text)
2599
                            }
2600
                        }
2601
                    };
2602813×
                    laps.lap("build: text diff");
2603813×
                    if !edits.is_empty() {
2604813×
                        self.edits.extend(edits);
2605813×
                        status = Some(ChangeStatus::Modified);
2606813×
                    }
26077497×
                }
26088312×
                let switched_to_opaque = wants_opaque;
26098312×
                if !switched_to_opaque && e.exec != expected.mode.exec {
26104×
                    if let Some(old) = mode_atom {
26114×
                        self.tree_ops.push(TreeOp::SetMode {
26124×
                            node,
26134×
                            old,
26144×
                            mode: Mode {
26154×
                                exec: e.exec,
26164×
                                opaque: false,
26174×
                            },
26184×
                        });
26194×
                        status.get_or_insert(ChangeStatus::ModeChanged);
26204×
                    }
26218308×
                }
2622
            }
2623224×
            FileContent::Opaque { blobs } => {
2624224×
                let new_ref = e.blob.unwrap_or_else(|| Repo::blob_ref(&e.content));
2625224×
                if e.kind == NodeKind::File && wants_opaque {
2626191×
                    // Unchanged or replaced, the file stays opaque: remember its blob.
2627191×
                    self.blobs.insert(path.to_string(), new_ref);
2628191×
                }
2629224×
                if blobs.len() > 1 && self.resolve.contains(path) && e.kind == expected.kind {
2630
                    // Settle the replace conflict (spec §9.3): replace every
2631
                    // live candidate with the chosen blob, depending on each
2632
                    // change that made one live, so all are superseded.
26332×
                    let chosen = self.blob_of(repo, path, e, nocompress)?;
26344×
                    for b in blobs {
26354×
                        self.edits.push(Edit::Replace {
26364×
                            node,
26374×
                            old: Some(*b),
26384×
                            new: Some(chosen),
26394×
                        });
26404×
                    }
26412×
                    self.contributors
26422×
                        .extend(self.base.blob_contributors(&node, blobs));
26432×
                    self.resolves = true;
26442×
                    self.statuses.push(PathStatus {
26452×
                        path: path.to_string(),
26462×
                        status: ChangeStatus::Resolved,
26472×
                    });
26482×
                    return Ok(());
2649222×
                }
2650222×
                if blobs.len() > 1 {
26512×
                    if !blobs.contains(&new_ref) {
26520×
                        self.statuses.push(PathStatus {
26530×
                            path: path.to_string(),
26540×
                            status: ChangeStatus::ConflictEdited,
26550×
                        });
26562×
                    }
26572×
                    return Ok(());
2658220×
                }
2659220×
                let current = blobs.first().copied();
2660220×
                if current != Some(new_ref) {
266134×
                    if e.kind == NodeKind::File && !wants_opaque {
2662
                        // Opaque → text (spec §7.5).
26632×
                        if let Some(old) = mode_atom {
26642×
                            self.tree_ops.push(TreeOp::SetMode {
26652×
                                node,
26662×
                                old,
26672×
                                mode: Mode {
26682×
                                    exec: e.exec,
26692×
                                    opaque: false,
26702×
                                },
26712×
                            });
26722×
                        }
26732×
                        self.edits.push(Edit::Replace {
26742×
                            node,
26752×
                            old: current,
26762×
                            new: None,
26772×
                        });
26782×
                        if !e.content.is_empty() {
26792×
                            self.edits.push(Edit::Insert {
26802×
                                node,
26812×
                                after: crate::objects::AtomRef::Start,
26822×
                                before: crate::objects::AtomRef::End,
26832×
                                lines: crate::lines::Lines::split(&e.content),
26842×
                            });
26852×
                        }
2686
                    } else {
268732×
                        let blob = self.blob_of(repo, path, e, nocompress)?;
268832×
                        self.edits.push(Edit::Replace {
268932×
                            node,
269032×
                            old: current,
269132×
                            new: Some(blob),
269232×
                        });
2693
                        // New contents and a new executable bit at once: the
2694
                        // bit was dropped (VS Code's inno_updater.exe,
2695
                        // 2018-02-28, lost +x for 49 commits; oxygen#87).
269632×
                        if e.kind == NodeKind::File && e.exec != expected.mode.exec {
26972×
                            if let Some(old) = mode_atom {
26982×
                                self.tree_ops.push(TreeOp::SetMode {
26992×
                                    node,
27002×
                                    old,
27012×
                                    mode: Mode {
27022×
                                        exec: e.exec,
27032×
                                        opaque: true,
27042×
                                    },
27052×
                                });
27062×
                            }
270730×
                        }
2708
                    }
270934×
                    status = Some(ChangeStatus::Modified);
2710186×
                } else if e.kind == NodeKind::File && e.exec != expected.mode.exec {
27110×
                    if let Some(old) = mode_atom {
27120×
                        self.tree_ops.push(TreeOp::SetMode {
27130×
                            node,
27140×
                            old,
27150×
                            mode: Mode {
27160×
                                exec: e.exec,
27170×
                                opaque: true,
27180×
                            },
27190×
                        });
27200×
                        status = Some(ChangeStatus::ModeChanged);
27210×
                    }
2722186×
                }
2723
            }
2724
        }
27258532×
        if let Some(s) = status {
2726853×
            self.statuses.push(PathStatus {
2727853×
                path: path.to_string(),
2728853×
                status: s,
2729853×
            });
27307679×
        }
27318532×
        Ok(())
273212818×
    }
2733
}
2734
27352×
fn repo_live_keys(base: &State, node: &NodeId) -> Vec<crate::graph::AtomKey> {
27362×
    base.live_lines(node).into_iter().map(|(k, _)| k).collect()
27372×
}
2738
2739
/// The changes a change's scope references (spec §8.1): introducers of every
2740
/// atom, placement, and mode it touches, and of every node it operates on.
27412134×
pub fn referenced_changes(change: &Change, base: &State) -> BTreeSet<ChangeHash> {
27422134×
    let mut out = BTreeSet::new();
27432134×
    let opref = |r: &OpRef, out: &mut BTreeSet<ChangeHash>| {
2744177×
        if !r.change.is_zero() {
2745177×
            out.insert(r.change);
2746177×
        }
2747177×
    };
27482134×
    let mut nodes: Vec<NodeId> = Vec::new();
27496577×
    for op in &change.tree_ops {
27506577×
        match op {
27516400×
            TreeOp::AddNode { parent, .. } => nodes.push(*parent),
2752129×
            TreeOp::RemoveNode { node, placement } => {
2753129×
                nodes.push(*node);
2754129×
                opref(placement, &mut out);
2755129×
            }
2756
            TreeOp::Move {
275736×
                node, old, parent, ..
275836×
            } => {
275936×
                nodes.push(*node);
276036×
                nodes.push(*parent);
276136×
                opref(old, &mut out);
276236×
            }
276312×
            TreeOp::SetMode { node, old, .. } => {
276412×
                nodes.push(*node);
276512×
                opref(old, &mut out);
276612×
            }
2767
        }
2768
    }
27698186×
    for e in &change.edits {
27708186×
        nodes.push(e.node());
2771134×
        match e {
27727473×
            Edit::Insert { after, before, .. } => {
277314946×
                for r in [after, before] {
277414946×
                    if let crate::objects::AtomRef::Atom { change, .. } = r {
27752185×
                        if !change.is_zero() {
27762182×
                            out.insert(*change);
27772182×
                        }
277812761×
                    }
2779
                }
2780
            }
2781579×
            Edit::Delete { atoms, .. } => {
2782
                // Deleting an atom also depends on whoever inserted the live
2783
                // atoms right after it: the author saw those insertions, and
2784
                // without the dependency spec §9.1 would call the deletion
2785
                // concurrent with them (a delete/insert conflict).
2786579×
                let graph = base.file(&e.node());
27871140×
                for r in atoms {
27881140×
                    if !r.change.is_zero() {
27891139×
                        out.insert(r.change);
27901139×
                    }
27911140×
                    if let Some(g) = graph {
27921173×
                        for i in r.from..r.to {
27931173×
                            out.extend(
27941173×
                                g.live_successor_changes(&(r.change, i))
27951173×
                                    .into_iter()
27961173×
                                    .filter(|c| !c.is_zero()),
2797
                            );
2798
                        }
27990×
                    }
2800
                }
2801
            }
2802
            // Replacing a blob depends on the change that made it current,
2803
            // as deleting a line depends on the line's introducer.
2804
            Edit::Replace {
280539×
                node,
280639×
                old: Some(old),
2807
                ..
2808
            } => {
280939×
                out.extend(
281039×
                    base.live_replaces(node)
281139×
                        .into_iter()
281246×
                        .filter(|(c, b)| b == old && !c.is_zero())
281339×
                        .map(|(c, _)| c),
2814
                );
2815
            }
281695×
            Edit::Replace { .. } => {}
2817
        }
2818
    }
281914799×
    for n in nodes {
282014799×
        if let Some(h) = base.tree().introducer(&n) {
28212230×
            out.insert(h);
282212569×
        }
2823
    }
28242134×
    out
28252134×
}
2826
2827
/// Paths of the nodes a change adds on top of `base_snapshot`.
28281336×
pub fn added_node_paths(
28291336×
    change: &Change,
28301336×
    base_snapshot: &Snapshot,
28311336×
) -> HashMap<String, (NodeId, NodeKind)> {
28321336×
    let by_node = base_snapshot.tree.by_node();
28331336×
    let mut new_names: HashMap<NodeId, (NodeId, String, NodeKind)> = HashMap::new();
28341336×
    for op in &change.tree_ops {
2835
        if let TreeOp::AddNode {
283684×
            node,
283784×
            parent,
283884×
            name,
283984×
            kind,
2840
            ..
2841108×
        } = op
284284×
        {
284384×
            new_names.insert(*node, (*parent, name.clone(), *kind));
284484×
        }
2845
    }
28461336×
    let mut out = HashMap::new();
28471336×
    for (node, (_, _, kind)) in &new_names {
284884×
        if let Some(path) = path_of_node(node, &by_node, &new_names) {
284984×
            out.insert(path, (*node, *kind));
285084×
        }
2851
    }
28521336×
    out
28531336×
}
2854
2855204×
fn path_of_node(
2856204×
    n: &NodeId,
2857204×
    by_node: &HashMap<NodeId, &crate::tree::TreeEntry>,
2858204×
    new_names: &HashMap<NodeId, (NodeId, String, NodeKind)>,
2859204×
) -> Option<String> {
2860204×
    if let Some(e) = by_node.get(n) {
286162×
        return Some(e.path.clone());
2862142×
    }
2863142×
    let mut parts = Vec::new();
2864142×
    let mut cur = *n;
2865
    loop {
2866283×
        if cur.is_root() {
2867128×
            break;
2868155×
        }
2869155×
        if let Some(e) = by_node.get(&cur) {
28700×
            if !e.path.is_empty() {
28710×
                parts.push(e.path.clone());
28720×
            }
28730×
            break;
2874155×
        }
2875155×
        let (parent, name, _) = new_names.get(&cur)?;
2876141×
        parts.push(name.clone());
2877141×
        cur = *parent;
2878
    }
2879128×
    parts.reverse();
2880128×
    Some(parts.join("/"))
2881204×
}
2882
2883
/// Render a change's operations with line content, using `base` for deleted lines.
288429×
pub fn render_change(change: &Change, base: &State, base_snapshot: &Snapshot) -> String {
288529×
    let by_node = base_snapshot.tree.by_node();
288629×
    let mut new_names: HashMap<NodeId, (NodeId, String, NodeKind)> = HashMap::new();
288729×
    for op in &change.tree_ops {
2888
        if let TreeOp::AddNode {
288915×
            node,
289015×
            parent,
289115×
            name,
289215×
            kind,
2893
            ..
289422×
        } = op
289515×
        {
289615×
            new_names.insert(*node, (*parent, name.clone(), *kind));
289715×
        }
2898
    }
289966×
    let path_of = |n: &NodeId| -> String {
290066×
        path_of_node(n, &by_node, &new_names).unwrap_or_else(|| format!("<node {}>", n.short(8)))
290166×
    };
2902
290329×
    let mut out = String::new();
290429×
    for op in &change.tree_ops {
290522×
        match op {
2906
            TreeOp::AddNode {
290715×
                node, kind, mode, ..
2908
            } => {
290915×
                out.push_str(&format!(
2910
                    "A {} ({:?}{}{})\n",
291115×
                    path_of(node),
2912
                    kind,
291315×
                    if mode.exec { ", exec" } else { "" },
291415×
                    if mode.opaque { ", opaque" } else { "" }
2915
                ));
2916
            }
29175×
            TreeOp::RemoveNode { node, .. } => out.push_str(&format!("D {}\n", path_of(node))),
2918
            TreeOp::Move {
29192×
                node, parent, name, ..
2920
            } => {
29212×
                let to = if parent.is_root() {
29220×
                    name.clone()
2923
                } else {
29242×
                    format!("{}/{name}", path_of(parent))
2925
                };
29262×
                out.push_str(&format!("R {} -> {to}\n", path_of(node)))
2927
            }
29280×
            TreeOp::SetMode { node, mode, .. } => {
29290×
                out.push_str(&format!(
29300×
                    "M {} mode: exec={} opaque={}\n",
29310×
                    path_of(node),
29320×
                    mode.exec,
29330×
                    mode.opaque
29340×
                ));
29350×
            }
2936
        }
2937
    }
2938
    // Every other file the change edits is modified: the summary names
2939
    // every path it touches, not only those with a tree operation.
294029×
    let with_op: HashSet<NodeId> = change.tree_ops.iter().map(|o| o.node()).collect();
294129×
    let mut listed: HashSet<NodeId> = HashSet::new();
294232×
    for e in &change.edits {
294332×
        let n = e.node();
294432×
        if !with_op.contains(&n) && listed.insert(n) {
294518×
            out.push_str(&format!("M {}\n", path_of(&n)));
294618×
        }
2947
    }
294829×
    let mut last_node: Option<NodeId> = None;
294932×
    for e in &change.edits {
295032×
        let n = e.node();
295132×
        if last_node != Some(n) {
295224×
            out.push_str(&format!("--- {}\n", path_of(&n)));
295324×
            last_node = Some(n);
295424×
        }
295532×
        match e {
295624×
            Edit::Insert { lines, .. } => {
295729×
                for l in lines.iter() {
295829×
                    out.push('+');
295929×
                    out.push_str(&String::from_utf8_lossy(l));
296029×
                    if !l.ends_with(b"\n") {
29610×
                        out.push('\n');
296229×
                    }
2963
                }
2964
            }
29657×
            Edit::Delete { atoms, .. } => {
29667×
                for r in atoms {
29677×
                    for i in r.from..r.to {
29687×
                        let content = base
29697×
                            .file(&n)
29707×
                            .and_then(|g| g.content(&(r.change, i)))
29717×
                            .unwrap_or(b"<unknown>\n");
29727×
                        out.push('-');
29737×
                        out.push_str(&String::from_utf8_lossy(content));
29747×
                        if !content.ends_with(b"\n") {
29750×
                            out.push('\n');
29767×
                        }
2977
                    }
2978
                }
2979
            }
29801×
            Edit::Replace { old, new, .. } => {
29811×
                out.push_str(&format!(
2982
                    "  opaque content {} -> {}\n",
29831×
                    old.map(|o| o.short(12)).unwrap_or_else(|| "none".into()),
29841×
                    new.map(|o| o.short(12)).unwrap_or_else(|| "none".into())
2985
                ));
2986
            }
2987
        }
2988
    }
298929×
    out
299029×
}
2991
2992
/// Pair removed files with added files of the same kind whose contents are
2993
/// at least `RENAME_THRESHOLD` percent alike, best matches first. Beyond
2994
/// `RENAME_LIMIT` candidate pairs only identical contents are paired.
2995
const RENAME_THRESHOLD: u32 = 50;
2996
const RENAME_LIMIT: usize = 10_000;
2997
2998
/// How a change is built beyond its entries: for a Git import, the seed
2999
/// its new nodes' identifiers derive from; and, when it is built from part
3000
/// of the tree, whether the new tree has anything under a directory.
3001
#[derive(Default)]
3002
struct Scope<'a> {
3003
    seed: Option<[u8; 32]>,
3004
    dir_needed: Option<&'a dyn Fn(&str) -> bool>,
3005
}
3006
30072050×
fn detect_renames(
30082050×
    base_snapshot: &Snapshot,
30092050×
    fs_entries: &BTreeMap<String, FsEntry>,
30102050×
    in_scope: impl Fn(&str) -> bool,
30112050×
) -> Vec<(String, String)> {
30122050×
    let removed: Vec<(&String, &crate::state::FileState)> = base_snapshot
30132050×
        .files
30142050×
        .iter()
301513356×
        .filter(|(p, f)| {
301613356×
            f.kind != NodeKind::Dir
301712928×
                && !fs_entries.contains_key(*p)
3018143×
                && in_scope(p)
3019133×
                && !f.is_conflicted()
302013356×
        })
30212050×
        .collect();
30222050×
    let added: Vec<(&String, &FsEntry)> = fs_entries
30232050×
        .iter()
302418859×
        .filter(|(p, e)| e.kind != NodeKind::Dir && !base_snapshot.files.contains_key(*p))
30252050×
        .collect();
30262050×
    if removed.is_empty() || added.is_empty() {
30271992×
        return vec![];
302858×
    }
302958×
    let exact_only = removed.len() * added.len() > RENAME_LIMIT;
303073×
    let old_content = |f: &crate::state::FileState| -> Option<Vec<u8>> {
303173×
        match &f.content {
303269×
            FileContent::Text { bytes, .. } => Some(bytes.clone()),
30334×
            _ => None,
3034
        }
303573×
    };
303658×
    let mut scored: Vec<(u32, usize, usize)> = Vec::new();
303773×
    for (ri, (_, f)) in removed.iter().enumerate() {
303873×
        let old = old_content(f);
303973×
        let old_blob = match &f.content {
30404×
            FileContent::Opaque { blobs } if blobs.len() == 1 => Some(blobs[0]),
304169×
            _ => None,
3042
        };
304392×
        for (ai, (_, e)) in added.iter().enumerate() {
304492×
            if e.kind != f.kind {
30450×
                continue;
304692×
            }
304792×
            let score = match (&old, old_blob) {
304882×
                (Some(o), _) => {
304982×
                    if exact_only {
30500×
                        if *o == *e.content {
30510×
                            100
3052
                        } else {
30530×
                            0
3054
                        }
3055
                    } else {
305682×
                        similarity(o, &e.content)
3057
                    }
3058
                }
305910×
                (None, Some(b)) if e.blob.unwrap_or_else(|| Repo::blob_ref(&e.content)) == b => 100,
30607×
                _ => 0,
3061
            };
306292×
            if score >= RENAME_THRESHOLD {
306339×
                scored.push((score, ri, ai));
306453×
            }
3065
        }
3066
    }
306758×
    scored.sort_by(|a, b| {
30689×
        b.0.cmp(&a.0)
30699×
            .then(removed[a.1].0.cmp(removed[b.1].0))
30709×
            .then(added[a.2].0.cmp(added[b.2].0))
30719×
    });
307258×
    let mut used_r = vec![false; removed.len()];
307358×
    let mut used_a = vec![false; added.len()];
307458×
    let mut out = Vec::new();
307558×
    for (_, ri, ai) in scored {
307639×
        if used_r[ri] || used_a[ai] {
30776×
            continue;
307833×
        }
307933×
        used_r[ri] = true;
308033×
        used_a[ai] = true;
308133×
        out.push((removed[ri].0.clone(), added[ai].0.clone()));
3082
    }
308358×
    out
30842050×
}
3085
3086
/// Percentage of the larger file's bytes found in lines the two share
3087
/// (multiset intersection of lines), like Git's similarity index.
308886×
pub fn similarity(a: &[u8], b: &[u8]) -> u32 {
308986×
    if a == b {
309022×
        return 100;
309164×
    }
309264×
    if a.is_empty() || b.is_empty() {
30933×
        return 0;
309461×
    }
309561×
    let mut counts: HashMap<&[u8], usize> = HashMap::new();
3096118×
    for l in split_lines(a) {
3097118×
        *counts.entry(l).or_default() += 1;
3098118×
    }
309961×
    let mut shared = 0usize;
3100113×
    for l in split_lines(b) {
3101113×
        if let Some(n) = counts.get_mut(l) {
310244×
            if *n > 0 {
310344×
                *n -= 1;
310444×
                shared += l.len();
310544×
            }
310669×
        }
3107
    }
310861×
    (shared * 100 / a.len().max(b.len())) as u32
310986×
}
3110
3111
#[cfg(test)]
3112
mod join_tests {
3113
    use super::*;
3114
3115
    #[test]
31161×
    fn missing_from_finds_what_the_other_lacks() {
31171×
        let a: BTreeMap<String, u8> = [("a", 1), ("b", 2), ("c/d", 3), ("e", 4)]
31181×
            .into_iter()
31194×
            .map(|(k, v)| (k.to_string(), v))
31201×
            .collect();
31211×
        let b: BTreeMap<String, ()> = [("0", ()), ("b", ()), ("c", ()), ("e", ())]
31221×
            .into_iter()
31234×
            .map(|(k, v)| (k.to_string(), v))
31241×
            .collect();
31251×
        let got: Vec<&str> = missing_from(&a, &b)
31261×
            .iter()
31272×
            .map(|(k, _)| k.as_str())
31281×
            .collect();
31291×
        assert_eq!(got, vec!["a", "c/d"]);
31301×
    }
3131
3132
    #[test]
31331×
    fn join_pairs_every_key_in_order() {
31341×
        let a: BTreeMap<String, u8> = [("a", 1), ("b/c", 2), ("d", 3)]
31351×
            .into_iter()
31363×
            .map(|(k, v)| (k.to_string(), v))
31371×
            .collect();
31381×
        let b: BTreeMap<String, char> = [("b", 'x'), ("b/c", 'y'), ("e", 'z')]
31391×
            .into_iter()
31403×
            .map(|(k, v)| (k.to_string(), v))
31411×
            .collect();
31421×
        let got: Vec<(String, Option<u8>, Option<char>)> = join(&a, &b)
31435×
            .map(|(k, x, y)| (k.clone(), x.copied(), y.copied()))
31441×
            .collect();
31451×
        let want = vec![
31461×
            ("a".to_string(), Some(1), None),
31471×
            ("b".to_string(), None, Some('x')),
31481×
            ("b/c".to_string(), Some(2), Some('y')),
31491×
            ("d".to_string(), Some(3), None),
31501×
            ("e".to_string(), None, Some('z')),
3151
        ];
31521×
        assert_eq!(got, want);
31531×
    }
3154
}