OXYGENHUB

Read a change's metadata without its operations 4d6c9c1b358c

Reading a change's metadata (for log, signature checks and walks of the
dependency graph) decoded its skeleton, all of its tree operations and
edit headers: for the Linux tree's first change, 96 000 of them. It now
steps over both with the CBOR cursor (Change::decode_meta). And log
walks the changes' metadata instead of loading a history state, unless
the repository is shallow, where a checkpoint's metadata tells of
changes beyond the boundary. log -n 100 on the Linux tip takes 1.15 s
(from 1.6 to 1.7 s, and 28 s yesterday).

refs #35
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-30 03:20 UTC
Signature
✔ signed by Andy McCall
Hash
4d6c9c1b358cb39e29dc4afdd962b3efbebea2ae9934416e8ced44ffd8a9f54d
Depends on
252e8cb23fbc 2863456a0f6d 69816d688300 6f8aeb996558 75167672ebcd 982864068a59 a4fad5b085c7 e02bf0cd0533 e8d38e47e9d7 ff0af1ee299d
Depended on by
c6e69e4b8d8f 74b4a3c62ea4 79b5a90ffd43 ce14f0d68b15 4cce912a0953
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
8 atoms, 0 tree operations, 3 files

Browse the tree at this change

modified crates/oxy-core/src/objects.rs +38 −0
…
864 unchanged lines
865 865
    }
866 866
}
867 867
868 +
impl Change {
869 +
    /// A change's metadata alone: its tree operations and edits are
870 +
    /// stepped over, not decoded. For log, signature checks and walks of
871 +
    /// the dependency graph, which read nothing else.
872 +
    pub fn decode_meta(bytes: &[u8]) -> Result<crate::state::ChangeMeta, ObjectError> {
873 +
        let mut c = cbor::Cursor::new(bytes);
874 +
        let (major, n) = c.head()?;
875 +
        if major != 5 {
876 +
            return schema("change: expected map");
877 +
        }
878 +
        let mut fields = Map::new();
879 +
        let mut last: Option<u64> = None;
880 +
        for _ in 0..n {
881 +
            let k = map_key(&mut c, &mut last)?;
882 +
            if k == 6 || k == 7 {
883 +
                c.skip()?;
884 +
                fields.insert(k, Value::Array(Vec::new()));
885 +
            } else {
886 +
                fields.insert(k, c.value()?);
887 +
            }
888 +
        }
889 +
        if !c.at_end() {
890 +
            return Err(ObjectError::Cbor(cbor::CborError::TrailingBytes));
891 +
        }
892 +
        Ok(crate::state::ChangeMeta::of(&Change::from_value(
893 +
            &Value::Map(fields),
894 +
        )?))
895 +
    }
896 +
}
897 +
868 898
/// A canonical map key: an unsigned integer above the previous one.
869 899
fn map_key(c: &mut cbor::Cursor, last: &mut Option<u64>) -> Result<u64, ObjectError> {
870 900
    let (km, k) = c.head()?;
…
456 unchanged lines
1327 1357
            m.insert(0, Value::Uint(2));
1328 1358
        }
1329 1359
        assert!(Change::decode_skeleton(&cbor::encode(&v), me).is_err());
1360 +
    }
1361 +
1362 +
    #[test]
1363 +
    fn metadata_reads_without_the_operations() {
1364 +
        let c = with_insert(sample_change(), big_lines(500));
1365 +
        let (bytes, _) = c.encode_parts();
1366 +
        let m = Change::decode_meta(&bytes).unwrap();
1367 +
        assert_eq!(m, crate::state::ChangeMeta::of(&c));
1330 1368
    }
1331 1369
1332 1370
    #[test]
…
97 unchanged lines
modified crates/oxy-core/src/repo/mod.rs +61 −8
…
1371 unchanged lines
1372 1372
        let (wc_hash, _) = self.working_copy()?;
1373 1373
        let mut all = heads.clone();
1374 1374
        all.push(wc_hash);
1375 -
        let state = self.load_state_history(&all)?;
1376 1375
        let head_set: BTreeSet<ChangeHash> = heads.into_iter().collect();
1377 1376
        let mut entries: Vec<LogEntry> = Vec::new();
1378 -
        for (h, meta) in state.changes() {
1377 +
        // A shallow clone knows changes beyond its boundary from a
1378 +
        // checkpoint's metadata only: its log comes from the state.
1379 +
        let metas = if self.is_shallow() {
1380 +
            self.load_state_history(&all)?
1381 +
                .changes()
1382 +
                .map(|(h, m)| (*h, m.clone()))
1383 +
                .collect()
1384 +
        } else {
1385 +
            self.metas_of(&all)?
1386 +
        };
1387 +
        for (h, meta) in metas {
1379 1388
            entries.push(LogEntry {
1380 -
                hash: *h,
1381 -
                change: meta.clone(),
1382 -
                is_working_copy: *h == wc_hash,
1383 -
                is_head: head_set.contains(h),
1389 +
                hash: h,
1390 +
                change: meta,
1391 +
                is_working_copy: h == wc_hash,
1392 +
                is_head: head_set.contains(&h),
1384 1393
            });
1385 1394
        }
1386 1395
        entries.sort_by(|a, b| {
…
21 unchanged lines
1408 1417
    /// The log of the state with these heads, newest first, with no working
1409 1418
    /// copy involved (for hosted repositories and any explicit state).
1410 1419
    pub fn log_state(&self, heads: &[ChangeHash]) -> Result<Vec<LogEntry>> {
1411 -
        // The log needs change metadata only: no file graph is built.
1412 -
        Ok(self.log_of(&self.load_state_history(heads)?))
1420 +
        // The log needs change metadata only: neither graphs nor the tree.
1421 +
        if self.is_shallow() {
1422 +
            return Ok(self.log_of(&self.load_state_history(heads)?));
1423 +
        }
1424 +
        let metas = self.metas_of(heads)?;
1425 +
        let depended: BTreeSet<ChangeHash> = metas
1426 +
            .iter()
1427 +
            .flat_map(|(_, m)| m.deps.iter().copied())
1428 +
            .collect();
1429 +
        let mut entries: Vec<LogEntry> = metas
1430 +
            .into_iter()
1431 +
            .map(|(h, meta)| LogEntry {
1432 +
                hash: h,
1433 +
                is_head: !depended.contains(&h),
1434 +
                change: meta,
1435 +
                is_working_copy: false,
1436 +
            })
1437 +
            .collect();
1438 +
        entries.sort_by(|a, b| b.change.hlc.cmp(&a.change.hlc).then(b.hash.cmp(&a.hash)));
1439 +
        Ok(entries)
1440 +
    }
1441 +
1442 +
    /// Every change of the state with these heads and its metadata, read
1443 +
    /// without its operations ([`Store::get_change_meta`]). A change beyond
1444 +
    /// a shallow clone's boundary is left out.
1445 +
    pub fn metas_of(
1446 +
        &self,
1447 +
        heads: &[ChangeHash],
1448 +
    ) -> Result<Vec<(ChangeHash, crate::state::ChangeMeta)>> {
1449 +
        let boundary = self.boundary();
1450 +
        let mut seen: HashSet<ChangeHash> = HashSet::new();
1451 +
        let mut stack: Vec<ChangeHash> = heads.to_vec();
1452 +
        let mut out = Vec::new();
1453 +
        while let Some(h) = stack.pop() {
1454 +
            if !seen.insert(h) {
1455 +
                continue;
1456 +
            }
1457 +
            let meta = match self.store.get_change_meta(&h) {
1458 +
                Ok(m) => m,
1459 +
                Err(crate::store::StoreError::NotFound(_)) if boundary.contains(&h) => continue,
1460 +
                Err(e) => return Err(e.into()),
1461 +
            };
1462 +
            stack.extend(meta.deps.iter().copied());
1463 +
            out.push((h, meta));
1464 +
        }
1465 +
        Ok(out)
1413 1466
    }
1414 1467
1415 1468
    /// The log of a state already loaded, newest first.
…
256 unchanged lines
modified crates/oxy-core/src/store.rs +2 −1
…
941 unchanged lines
942 942
    /// without its contents: read from its skeleton, so neither its text
943 943
    /// blobs nor its inline lines are read (spec §11).
944 944
    pub fn get_change_meta(&self, r: &ObjectRef) -> Result<crate::state::ChangeMeta> {
945 -
        Ok(crate::state::ChangeMeta::of(&self.get_change_lazy(r)?.0))
945 +
        let bytes = self.get_typed(r, ObjectType::Change)?;
946 +
        Change::decode_meta(&bytes).map_err(|e| StoreError::Corrupt(*r, e.to_string()))
946 947
    }
947 948
948 949
    fn read_text(&self, r: &ObjectRef) -> Result<(Blob, Vec<u8>)> {
…
753 unchanged lines