OXYGENHUB

Status rewrites the manifest only when it helps, and asks keys on demand 992467d31d26

A snapshot rewrites the manifest only when it would teach the next one
something: other flags, a file found equal to the base, a new blob, or
a path added or gone. An edited file is read again next time whatever
the manifest says, so a status of a few edits on the Linux tree no
longer writes 96 000 lines (0.15 to 0.35 s). A stale manifest is always
safe: a file whose stat changed is read.

The walk asks a text file's graph key only when the manifest knows the
file with the same stat and a key, rather than being handed every key
of the base worked out in advance (0.09 s); the snapshot keeps the keys
of the files it read.

refs #41
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-30 10:53 UTC
Signature
✔ signed by Andy McCall
Hash
992467d31d263dd85ae5804816f5d39ee7a0421d28a3c9bed735a17b6e77d7c1
Depends on
0bdc3f91a39f 75167672ebcd 8b276e0106d1 982864068a59 c3069537d7c5 cc7a00565161 d0990c2bff74 d0d52107b074 e7ddd552b619
Depended on by
236f35c0baba cd4eb55e3b3d 3604179679eb f9466021286a 1966f5db15ee
In bookmarks
agents-in-readme bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
In tags
v0.1.0 v0.10.0 v0.11.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
Edits
11 atoms, 0 tree operations, 2 files

Browse the tree at this change

modified crates/oxy-core/src/repo/mod.rs +15 −5
…
129 unchanged lines
130 130
    pub is_head: bool,
131 131
}
132 132
133 +
/// A text file's base graph key, by path, as a walk asks for it.
134 +
pub(crate) type KeyOf<'a> = dyn Fn(&str) -> Option<String> + Sync + 'a;
135 +
133 136
/// A file as found on disk or in an imported tree.
134 137
pub(crate) struct FsEntry {
135 138
    pub kind: NodeKind,
…
1019 unchanged lines
1155 1158
        &self,
1156 1159
        cache: Option<&prompt::Manifest>,
1157 1160
        tracked: &BTreeMap<String, (NodeKind, Option<ObjectRef>)>,
1158 -
        keys: Option<&HashMap<String, String>>,
1161 +
        keys: Option<&KeyOf>,
1159 1162
    ) -> Result<BTreeMap<String, FsEntry>> {
1160 1163
        let nested = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1161 1164
        let wb = self.walker(&nested);
…
83 unchanged lines
1245 1248
                    .filter(|(r, _, _)| is_attributes(r))
1246 1249
                    .all(|(rel, _, meta)| {
1247 1250
                        stat_cached(rel, meta, cache)
1248 -
                            .is_some_and(|e| e.graph.is_some() && e.graph.as_ref() == k.get(rel))
1251 +
                            .is_some_and(|e| e.graph.is_some() && e.graph == k(rel))
1249 1252
                    });
1250 1253
            let none_removed = tracked
1251 1254
                .keys()
…
7 unchanged lines
1259 1262
            let read: Vec<(String, FsEntry)> = files
1260 1263
                .par_iter()
1261 1264
                .map(|(rel, path, meta)| {
1265 +
                    // A key is worked out only for a file the manifest
1266 +
                    // knows with the same stat and a key of its own.
1262 1267
                    let key = keys
1263 1268
                        .filter(|_| !is_attributes(rel))
1264 -
                        .and_then(|k| k.get(rel))
1265 -
                        .map(String::as_str);
1266 -
                    Ok((rel.clone(), read_file_entry(rel, path, meta, cache, key)?))
1269 +
                        .filter(|_| {
1270 +
                            stat_cached(rel, meta, cache).is_some_and(|e| e.graph.is_some())
1271 +
                        })
1272 +
                        .and_then(|k| k(rel));
1273 +
                    Ok((
1274 +
                        rel.clone(),
1275 +
                        read_file_entry(rel, path, meta, cache, key.as_deref())?,
1276 +
                    ))
1267 1277
                })
1268 1278
                .collect::<Result<Vec<_>>>()?;
1269 1279
            for (rel, mut e) in read {
…
446 unchanged lines
modified crates/oxy-core/src/repo/snapshot.rs +40 −18
…
291 unchanged lines
292 292
            let _t = crate::timing::phase("snapshot: tree view");
293 293
            base.tree().view()
294 294
        };
295 -
        let keys_phase = crate::timing::phase("snapshot: graph keys");
296 -
        let keys: HashMap<String, String> = view
297 -
            .entries
298 -
            .iter()
299 -
            .filter(|(_, e)| e.kind == NodeKind::File && !e.mode.opaque)
300 -
            .filter_map(|(p, e)| {
301 -
                let k = base.graph_key(&e.node)?;
302 -
                Some((p.clone(), crate::hash::ObjectRef(k).to_hex()))
303 -
            })
304 -
            .collect();
305 -
        drop(keys_phase);
306 295
        let manifest = {
307 296
            let _t = crate::timing::phase("snapshot: read manifest");
308 297
            self.read_manifest()?.map(|(_, _, m)| m)
…
14 unchanged lines
323 312
            }
324 313
            t
325 314
        };
315 +
        // A text file's graph key, worked out when the walk asks for it.
316 +
        let key_of = |p: &str| -> Option<String> {
317 +
            let e = view.entries.get(p)?;
318 +
            if e.kind != NodeKind::File || e.mode.opaque {
319 +
                return None;
320 +
            }
321 +
            Some(crate::hash::ObjectRef(base.graph_key(&e.node)?).to_hex())
322 +
        };
326 323
        let mut fs_entries = {
327 324
            let _t = crate::timing::phase("snapshot: read working copy");
328 -
            self.walk_working_copy_with(manifest.as_deref(), &tracked, Some(&keys))?
325 +
            self.walk_working_copy_with(manifest.as_deref(), &tracked, Some(&key_of))?
329 326
        };
327 +
        // The keys of the files read, before building graphs drops them: a
328 +
        // file found equal to its base rendering keeps its key.
329 +
        let keys: HashMap<String, String> = fs_entries
330 +
            .iter()
331 +
            .filter(|(_, e)| !e.unchanged && e.kind == NodeKind::File)
332 +
            .filter_map(|(p, _)| Some((p.clone(), key_of(p)?)))
333 +
            .collect();
330 334
        let base_snapshot = {
331 335
            let _t = crate::timing::phase("snapshot: load and render base");
332 336
            if partial {
…
81 unchanged lines
414 418
        drop(record);
415 419
        // The prompt compares against this without recording anything.
416 420
        let _t = crate::timing::phase("snapshot: write manifest");
417 -
        self.write_manifest_entries(
418 -
            base_snapshot.is_conflicted(),
419 -
            !wc.tree_ops.is_empty() || !wc.edits.is_empty(),
420 -
            &fs_entries,
421 -
            &built.blobs,
422 -
        )?;
421 +
        let conflicted = base_snapshot.is_conflicted();
422 +
        let uncommitted = !wc.tree_ops.is_empty() || !wc.edits.is_empty();
423 +
        // Rewriting the manifest pays only when it would teach the next
424 +
        // snapshot something: other flags, a file found equal to the base
425 +
        // (it need not be read again), a new blob, a path added or gone.
426 +
        // An edited file is read next time whatever it says.
427 +
        let worth = match self.read_manifest()? {
428 +
            None => true,
429 +
            Some((c, u, m)) => {
430 +
                c != conflicted
431 +
                    || u != uncommitted
432 +
                    || m.entries.len() != fs_entries.len()
433 +
                    || fs_entries.iter().any(|(p, e)| {
434 +
                        !e.unchanged && (e.graph_key.is_some() || !m.entries.contains_key(p))
435 +
                    })
436 +
                    || built
437 +
                        .blobs
438 +
                        .iter()
439 +
                        .any(|(p, b)| m.entries.get(p).and_then(|e| e.blob) != Some(*b))
440 +
            }
441 +
        };
442 +
        if worth {
443 +
            self.write_manifest_entries(conflicted, uncommitted, &fs_entries, &built.blobs)?;
444 +
        }
423 445
        Ok(SnapshotResult {
424 446
            wc_hash: new_hash,
425 447
            wc,
…
1929 unchanged lines