OXYGENHUB

A command parses the manifest once d0990c2bff74

The parsed manifest is kept for the rest of the command while its file
is the same one (size, time and inode), and dropped whenever it is
written or removed: a status that found the working copy dirty no
longer parses it a second time for the snapshot (0.1 s on the Linux
tree). The walks take a path under the root as it is where the
separator is already a slash, rather than splitting and joining it.
Snapshot times say how long building graphs and rendering take apart.

refs #41
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-30 10:41 UTC
Signature
✔ signed by Andy McCall
Hash
d0990c2bff74c79d895c2a4d5f978455d138c133a85610bceb3303fb39b869b7
Depends on
135212de5370 4dee277c327a 75167672ebcd 86e1824f2d0b 8b276e0106d1 982864068a59 b7f102fcbb17 cc7a00565161 eaf3e9068180
Depended on by
236f35c0baba 0389c3e82f6c 33472ccc08cc d616faf5d136 1966f5db15ee 76740263089c 992467d31d26
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
18 atoms, 0 tree operations, 3 files

Browse the tree at this change

modified crates/oxy-core/src/repo/mod.rs +17 −7
…
167 unchanged lines
168 168
pub struct Repo {
169 169
    /// Oxygen-pinned subrepos skipped by the running Git export.
170 170
    pub(crate) git_export_skipped: std::cell::Cell<usize>,
171 +
    /// The manifest as last read, with the file identity it was read from,
172 +
    /// so one command parses it once (see `read_manifest`).
173 +
    pub(crate) manifest_cache: std::cell::RefCell<Option<prompt::CachedManifest>>,
171 174
    root: PathBuf,
172 175
    oxy: PathBuf,
173 176
    store: Store,
…
71 unchanged lines
245 248
        let store = Store::open(oxy.join("objects"))?;
246 249
        let repo = Repo {
247 250
            git_export_skipped: Default::default(),
251 +
            manifest_cache: Default::default(),
248 252
            root,
249 253
            oxy,
250 254
            store,
…
43 unchanged lines
294 298
        let store = Store::open(oxy.join("objects"))?;
295 299
        let repo = Repo {
296 300
            git_export_skipped: Default::default(),
301 +
            manifest_cache: Default::default(),
297 302
            root,
298 303
            oxy,
299 304
            store,
…
65 unchanged lines
365 370
                let store = Store::open(shared.join("objects"))?;
366 371
                let mut repo = Repo {
367 372
                    git_export_skipped: Default::default(),
373 +
                    manifest_cache: Default::default(),
368 374
                    root: d.to_path_buf(),
369 375
                    oxy: shared,
370 376
                    store,
…
20 unchanged lines
391 397
                let store = Store::open(oxy.join("objects"))?;
392 398
                let mut repo = Repo {
393 399
                    git_export_skipped: Default::default(),
400 +
                    manifest_cache: Default::default(),
394 401
                    root: d.to_path_buf(),
395 402
                    oxy,
396 403
                    store,
…
773 unchanged lines
1170 1177
                    if path == root {
1171 1178
                        return Ok(None);
1172 1179
                    }
1173 -
                    let rel: String = path
1174 -
                        .strip_prefix(root)
1175 -
                        .unwrap()
1176 -
                        .components()
1177 -
                        .map(|c| c.as_os_str().to_string_lossy().to_string())
1178 -
                        .collect::<Vec<_>>()
1179 -
                        .join("/");
1180 +
                    let under = path.strip_prefix(root).unwrap();
1181 +
                    // Already `/`-separated where that is the separator.
1182 +
                    let rel: String = match under.to_str() {
1183 +
                        Some(s) if std::path::MAIN_SEPARATOR == '/' => s.to_string(),
1184 +
                        _ => under
1185 +
                            .components()
1186 +
                            .map(|c| c.as_os_str().to_string_lossy().to_string())
1187 +
                            .collect::<Vec<_>>()
1188 +
                            .join("/"),
1189 +
                    };
1180 1190
                    let Some(ft) = entry.file_type() else {
1181 1191
                        return Ok(None);
1182 1192
                    };
…
515 unchanged lines
modified crates/oxy-core/src/repo/prompt.rs +46 −10
…
147 unchanged lines
148 148
    pub entries: BTreeMap<String, ManifestEntry>,
149 149
}
150 150
151 +
/// A parsed manifest and the identity (size, time, inode) of the file it
152 +
/// came from: reused while the file is the same one.
153 +
pub(crate) type CachedManifest = ((u64, u128, u64), (bool, bool, std::sync::Arc<Manifest>));
154 +
151 155
pub(crate) fn mtime_ns(m: &fs::Metadata) -> u128 {
152 156
    m.modified()
153 157
        .ok()
…
52 unchanged lines
206 210
        entries: &BTreeMap<String, FsEntry>,
207 211
        blobs: &HashMap<String, ObjectRef>,
208 212
    ) -> Result<()> {
213 +
        self.manifest_cache.take();
209 214
        let written = std::time::SystemTime::now()
210 215
            .duration_since(UNIX_EPOCH)
211 216
            .map(|d| d.as_nanos())
…
61 unchanged lines
273 278
        uncommitted: bool,
274 279
        keys: &HashMap<String, String>,
275 280
    ) -> Result<()> {
281 +
        self.manifest_cache.take();
276 282
        let p = self.manifest_path();
277 283
        let text = match fs::read_to_string(&p) {
278 284
            Ok(t) => t,
…
26 unchanged lines
305 311
    /// change without a snapshot (undo, rewrites); the prompt shows blank
306 312
    /// markers until the next snapshot.
307 313
    pub(crate) fn drop_manifest(&self) {
314 +
        self.manifest_cache.take();
308 315
        let _ = fs::remove_file(self.manifest_path());
316 +
    }
317 +
318 +
    /// The manifest, with whether it records a conflict and uncommitted
319 +
    /// work; parsed once while the file stays the same one.
320 +
    pub(crate) fn read_manifest(&self) -> Result<Option<(bool, bool, std::sync::Arc<Manifest>)>> {
321 +
        let path = self.manifest_path();
322 +
        let id = match fs::metadata(&path) {
323 +
            Ok(m) => {
324 +
                #[cfg(unix)]
325 +
                let ino = std::os::unix::fs::MetadataExt::ino(&m);
326 +
                #[cfg(not(unix))]
327 +
                let ino = 0;
328 +
                (m.len(), mtime_ns(&m), ino)
329 +
            }
330 +
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
331 +
            Err(e) => return Err(e.into()),
332 +
        };
333 +
        if let Some((cached_id, m)) = &*self.manifest_cache.borrow() {
334 +
            if *cached_id == id {
335 +
                return Ok(Some(m.clone()));
336 +
            }
337 +
        }
338 +
        let Some((c, u, m)) = self.parse_manifest(&path)? else {
339 +
            return Ok(None);
340 +
        };
341 +
        let m = (c, u, std::sync::Arc::new(m));
342 +
        *self.manifest_cache.borrow_mut() = Some((id, m.clone()));
343 +
        Ok(Some(m))
309 344
    }
310 345
311 -
    pub(crate) fn read_manifest(&self) -> Result<Option<(bool, bool, Manifest)>> {
312 -
        let text = match fs::read_to_string(self.manifest_path()) {
346 +
    fn parse_manifest(&self, path: &std::path::Path) -> Result<Option<(bool, bool, Manifest)>> {
347 +
        let text = match fs::read_to_string(path) {
313 348
            Ok(t) => t,
314 349
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
315 350
            Err(e) => return Err(e.into()),
…
97 unchanged lines
413 448
                if ft.is_dir() {
414 449
                    return WalkState::Continue;
415 450
                }
416 -
                let rel: String = entry
417 -
                    .path()
418 -
                    .strip_prefix(root)
419 -
                    .unwrap()
420 -
                    .components()
421 -
                    .map(|c| c.as_os_str().to_string_lossy().to_string())
422 -
                    .collect::<Vec<_>>()
423 -
                    .join("/");
451 +
                let under = entry.path().strip_prefix(root).unwrap();
452 +
                let rel: String = match under.to_str() {
453 +
                    Some(s) if std::path::MAIN_SEPARATOR == '/' => s.to_string(),
454 +
                    _ => under
455 +
                        .components()
456 +
                        .map(|c| c.as_os_str().to_string_lossy().to_string())
457 +
                        .collect::<Vec<_>>()
458 +
                        .join("/"),
459 +
                };
424 460
                if !in_scope(&rel) {
425 461
                    return WalkState::Continue;
426 462
                }
…
137 unchanged lines
modified crates/oxy-core/src/repo/snapshot.rs +6 −2
…
321 unchanged lines
322 322
        };
323 323
        let mut fs_entries = {
324 324
            let _t = crate::timing::phase("snapshot: read working copy");
325 -
            self.walk_working_copy_with(manifest.as_ref(), &tracked, Some(&keys))?
325 +
            self.walk_working_copy_with(manifest.as_deref(), &tracked, Some(&keys))?
326 326
        };
327 327
        let base_snapshot = {
328 328
            let _t = crate::timing::phase("snapshot: load and render base");
…
9 unchanged lines
338 338
                    .filter(|e| e.kind == NodeKind::File && !skip.contains(&e.node))
339 339
                    .map(|e| e.node)
340 340
                    .collect();
341 -
                self.load_graphs_of(&mut base, &need)?;
341 +
                {
342 +
                    let _t = crate::timing::phase("snapshot:   of which building graphs");
343 +
                    self.load_graphs_of(&mut base, &need)?;
344 +
                }
345 +
                let _t = crate::timing::phase("snapshot:   of which rendering");
342 346
                base.materialize_except(&skip)
343 347
            } else {
344 348
                self.load_all_graphs(&mut base)?;
…
2003 unchanged lines