OXYGENHUB

Manifest as a flat binary file, hashes and graph keys as bytes (refs #41) 1966f5db15ee

The workspace manifest is now read in one pass with no per-line parse, and
content hashes and graph keys travel as 32-byte arrays rather than hex
strings through the walk, snapshot and checkout. On the Linux tree the
manifest shrinks from 19 MB to 12.9 MB and clean status takes 0.24 s,
level with Git. An older text manifest reads as none and is rewritten.
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-30 21:13 UTC
Signature
✔ signed by Andy McCall
Hash
1966f5db15ee42be2e5abc0301434dfe5d7371acd55e53664c8b4a02fbaf1f86
Depends on
0554aaa7ddef 135212de5370 40a40bb7b798 75167672ebcd 76740263089c 8457a6cc6984 86e1824f2d0b 982864068a59 992467d31d26 b7f102fcbb17 c5ce0367eddf cc7a00565161 d0990c2bff74 d3cd7ef95da6 da868d5d2646 dfb23a3a8cfa e7ddd552b619
Depended on by
94cdbc576ce1 e96a430c78f3 1ec3f008e96a 3604179679eb d4dbab97e948 f9466021286a
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.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
45 atoms, 0 tree operations, 4 files

Browse the tree at this change

modified crates/oxy-core/src/repo/checkout.rs +6 −6
…
102 unchanged lines
103 103
        // The manifest after the checkout, built from what is known.
104 104
        let plan = crate::timing::phase("checkout: plan");
105 105
        let mut entries: BTreeMap<String, FsEntry> = BTreeMap::new();
106 -
        let hex = |b: &[u8]| blake3::hash(b).to_hex().to_string();
106 +
        let hex = |b: &[u8]| *blake3::hash(b).as_bytes();
107 107
        // Text files to render, attribute files first (the others need them).
108 108
        let mut texts: Vec<(String, NodeId, Mode)> = Vec::new();
109 109
        for (path, e) in tview.entries.iter() {
…
12 unchanged lines
122 122
                // The manifest proves the file on disk equal to the rendering
123 123
                // of a graph whose key is the target's: nothing to read,
124 124
                // render or write.
125 -
                let key = target.graph_key(&e.node).map(|k| ObjectRef(k).to_hex());
125 +
                let key = target.graph_key(&e.node);
126 126
                let proven = |m: &&prompt::ManifestEntry| {
127 127
                    !is_attributes(path) && key.is_some() && m.graph == key
128 128
                };
…
2 unchanged lines
131 131
                        entries.insert(
132 132
                            path.clone(),
133 133
                            FsEntry {
134 -
                                content_hash: Some(m.hash.clone()),
135 -
                                graph_key: m.graph.clone(),
134 +
                                content_hash: Some(m.hash),
135 +
                                graph_key: m.graph,
136 136
                                unchanged: true,
137 137
                                stat: Some((m.size, m.mtime_ns)),
138 138
                                ..FsEntry::new(NodeKind::File, e.mode.exec, Vec::new())
…
33 unchanged lines
172 172
                entries.insert(
173 173
                    path.clone(),
174 174
                    FsEntry {
175 -
                        content_hash: Some(m.hash.clone()),
175 +
                        content_hash: Some(m.hash),
176 176
                        blob: (e.kind == NodeKind::File).then_some(blob),
177 177
                        stat: Some((m.size, m.mtime_ns)),
178 178
                        ..FsEntry::new(e.kind, e.mode.exec, Vec::new())
…
103 unchanged lines
282 282
                    FsEntry {
283 283
                        content_hash: Some(hex(&on_disk)),
284 284
                        graph_key: (!conflicted && a.eol.is_none())
285 -
                            .then(|| keys.get(node).map(|k| ObjectRef(*k).to_hex()))
285 +
                            .then(|| keys.get(node).copied())
286 286
                            .flatten(),
287 287
                        ..FsEntry::new(NodeKind::File, mode.exec, Vec::new())
288 288
                    },
…
169 unchanged lines
modified crates/oxy-core/src/repo/mod.rs +10 −13
…
131 unchanged lines
132 132
}
133 133
134 134
/// A text file's base graph key, by path, as a walk asks for it.
135 -
pub(crate) type KeyOf<'a> = dyn Fn(&str) -> Option<String> + Sync + 'a;
135 +
pub(crate) type KeyOf<'a> = dyn Fn(&str) -> Option<[u8; 32]> + Sync + 'a;
136 136
137 137
/// A file as found on disk or in an imported tree.
138 138
pub(crate) struct FsEntry {
…
8 unchanged lines
147 147
    pub blob: Option<ObjectRef>,
148 148
    /// BLAKE3 of the content, carried over from the manifest when the stat
149 149
    /// is unchanged, so the manifest can be rewritten without rehashing.
150 -
    pub content_hash: Option<String>,
150 +
    pub content_hash: Option<[u8; 32]>,
151 151
    /// A text file known equal to the base's rendering without reading it:
152 152
    /// its stat matches the manifest and the manifest recorded it equal to
153 153
    /// a base file graph with the same key (spec §12.3). `content` is empty.
154 154
    pub unchanged: bool,
155 155
    /// The base graph key this file equals, for the next manifest.
156 -
    pub graph_key: Option<String>,
156 +
    pub graph_key: Option<[u8; 32]>,
157 157
    /// Size and mtime already known to be current (a checkout leaving the
158 158
    /// file as it was), so the manifest need not stat it again.
159 159
    pub stat: Option<(u64, u128)>,
…
1066 unchanged lines
1226 1226
                            stat_cached(rel, meta, cache).is_some_and(|e| e.graph.is_some())
1227 1227
                        })
1228 1228
                        .and_then(|k| k(rel));
1229 -
                    Ok((
1230 -
                        rel.clone(),
1231 -
                        read_file_entry(rel, path, meta, cache, key.as_deref())?,
1232 -
                    ))
1229 +
                    Ok((rel.clone(), read_file_entry(rel, path, meta, cache, key)?))
1233 1230
                })
1234 1231
                .collect::<Result<Vec<_>>>()?;
1235 1232
            for (rel, mut e) in read {
…
417 unchanged lines
1653 1650
    path: &Path,
1654 1651
    meta: &fs::Metadata,
1655 1652
    cache: Option<&prompt::Manifest>,
1656 -
    key: Option<&str>,
1653 +
    key: Option<[u8; 32]>,
1657 1654
) -> Result<FsEntry> {
1658 1655
    let exec = is_executable(meta);
1659 1656
    let stat = Some((meta.len(), prompt::mtime_ns(meta)));
1660 1657
    let mut e = match stat_cached(rel, meta, cache) {
1661 1658
        Some(e) if e.blob.is_some() => FsEntry {
1662 1659
            blob: e.blob,
1663 -
            content_hash: Some(e.hash.clone()),
1660 +
            content_hash: Some(e.hash),
1664 1661
            ..FsEntry::new(NodeKind::File, exec, vec![])
1665 1662
        },
1666 -
        Some(e) if key.is_some() && e.graph.as_deref() == key => FsEntry {
1667 -
            content_hash: Some(e.hash.clone()),
1663 +
        Some(e) if key.is_some() && e.graph == key => FsEntry {
1664 +
            content_hash: Some(e.hash),
1668 1665
            unchanged: true,
1669 -
            graph_key: e.graph.clone(),
1666 +
            graph_key: e.graph,
1670 1667
            ..FsEntry::new(NodeKind::File, exec, vec![])
1671 1668
        },
1672 1669
        Some(e) => FsEntry {
1673 -
            content_hash: Some(e.hash.clone()),
1670 +
            content_hash: Some(e.hash),
1674 1671
            ..FsEntry::new(NodeKind::File, exec, fs::read(path)?)
1675 1672
        },
1676 1673
        None => FsEntry::new(NodeKind::File, exec, fs::read(path)?),
…
6 unchanged lines
modified crates/oxy-core/src/repo/prompt.rs +121 −123
…
130 unchanged lines
131 131
pub(crate) struct ManifestEntry {
132 132
    pub size: u64,
133 133
    pub mtime_ns: u128,
134 -
    pub hash: String,
134 +
    pub hash: [u8; 32],
135 135
    pub blob: Option<ObjectRef>,
136 136
    /// The graph key (`State::graph_key`) of the base file this text file
137 137
    /// was found equal to, rendered, byte for byte.
138 -
    pub graph: Option<String>,
138 +
    pub graph: Option<[u8; 32]>,
139 139
    /// Whether the file was executable: a mode change alters neither size
140 140
    /// nor time.
141 141
    pub exec: bool,
…
24 unchanged lines
166 166
/// A format with glyphs: icon, bookmark, change id, state, ahead/behind.
167 167
pub const GLYPH_PROMPT_FORMAT: &str = "%i %b %c %s%U%O";
168 168
169 -
/// One manifest line per file: size, mtime in nanoseconds, hash, blob ref
170 -
/// or `-`, graph key or `-`, `x` when executable or `-`, path.
171 -
fn manifest_line(
172 -
    path: &str,
173 -
    size: u64,
174 -
    mtime_ns: u128,
175 -
    hash: &str,
176 -
    blob: Option<ObjectRef>,
177 -
    graph: Option<&str>,
178 -
    exec: bool,
179 -
) -> String {
180 -
    format!(
181 -
        "{size}\t{mtime_ns}\t{hash}\t{}\t{}\t{}\t{path}\n",
182 -
        blob.map(|b| b.to_hex()).unwrap_or_else(|| "-".into()),
183 -
        graph.unwrap_or("-"),
184 -
        if exec { "x" } else { "-" }
185 -
    )
169 +
/// The manifest file (oxygen#41): a flat binary layout read in one pass.
170 +
/// A magic number; the conflicted and uncommitted flags; when it was
171 +
/// written, at a fixed offset so an otherwise equal manifest can be
172 +
/// recognised; then each entry in path order: path, size, mtime in
173 +
/// nanoseconds, BLAKE3 of the content, the stored blob of an opaque file,
174 +
/// the graph key it was found equal to, and whether it is executable. A
175 +
/// file in any other layout (an older text manifest) reads as no manifest,
176 +
/// and the next snapshot writes it again.
177 +
const MANIFEST_MAGIC: &[u8; 8] = b"oxyman1\0";
178 +
/// Where the written time sits: after the magic and the two flags.
179 +
const MANIFEST_WRITTEN: std::ops::Range<usize> = 10..26;
180 +
181 +
fn encode_manifest(
182 +
    conflicted: bool,
183 +
    uncommitted: bool,
184 +
    written_ns: u128,
185 +
    entries: &mut dyn Iterator<Item = (&str, ManifestEntry)>,
186 +
) -> Vec<u8> {
187 +
    let mut w = crate::flat::Writer::default();
188 +
    w.fixed(MANIFEST_MAGIC);
189 +
    w.u8(conflicted as u8);
190 +
    w.u8(uncommitted as u8);
191 +
    w.fixed(&written_ns.to_le_bytes());
192 +
    for (path, e) in entries {
193 +
        w.bytes(path.as_bytes());
194 +
        w.u64(e.size);
195 +
        w.fixed(&e.mtime_ns.to_le_bytes());
196 +
        w.fixed(&e.hash);
197 +
        w.opt_object(&e.blob);
198 +
        match e.graph {
199 +
            None => w.u8(0),
200 +
            Some(k) => {
201 +
                w.u8(1);
202 +
                w.fixed(&k);
203 +
            }
204 +
        }
205 +
        w.u8(e.exec as u8);
206 +
    }
207 +
    w.0
186 208
}
187 209
210 +
fn decode_manifest(buf: &[u8]) -> Option<(bool, bool, Manifest)> {
211 +
    let mut r = crate::flat::Reader::new(buf);
212 +
    if &r.fixed::<8>().ok()? != MANIFEST_MAGIC {
213 +
        return None;
214 +
    }
215 +
    let conflicted = r.u8().ok()? == 1;
216 +
    let uncommitted = r.u8().ok()? == 1;
217 +
    let written_ns = u128::from_le_bytes(r.fixed().ok()?);
218 +
    let mut entries = Vec::new();
219 +
    while !r.at_end() {
220 +
        let path = std::str::from_utf8(r.bytes().ok()?).ok()?.to_string();
221 +
        let e = ManifestEntry {
222 +
            size: r.u64().ok()?,
223 +
            mtime_ns: u128::from_le_bytes(r.fixed().ok()?),
224 +
            hash: r.fixed().ok()?,
225 +
            blob: r.opt_object().ok()?,
226 +
            graph: match r.u8().ok()? {
227 +
                0 => None,
228 +
                _ => Some(r.fixed().ok()?),
229 +
            },
230 +
            exec: r.u8().ok()? == 1,
231 +
        };
232 +
        entries.push((path, e));
233 +
    }
234 +
    // Written in path order, so this builds the map without searching.
235 +
    let entries = entries.into_iter().collect();
236 +
    Some((
237 +
        conflicted,
238 +
        uncommitted,
239 +
        Manifest {
240 +
            written_ns,
241 +
            entries,
242 +
        },
243 +
    ))
244 +
}
245 +
188 246
impl Repo {
189 247
    pub(crate) fn manifest_path(&self) -> PathBuf {
190 248
        self.oxy
…
25 unchanged lines
216 274
            .duration_since(UNIX_EPOCH)
217 275
            .map(|d| d.as_nanos())
218 276
            .unwrap_or(0);
219 -
        let head = format!(
220 -
            "# conflicted={}\n# uncommitted={}\n",
221 -
            conflicted as u8, uncommitted as u8
222 -
        );
223 -
        let mut body = String::new();
224 277
        let mut newest_ns: u128 = 0;
225 -
        for (path, e) in entries {
226 -
            let full = self.root.join(path);
278 +
        let mut rows = entries.iter().map(|(path, e)| {
227 279
            let (size, mtime_ns) = match e.stat {
228 280
                Some(known) => known,
229 -
                None => match fs::symlink_metadata(&full) {
281 +
                None => match fs::symlink_metadata(self.root.join(path)) {
230 282
                    Ok(m) => (m.len(), mtime_ns(&m)),
231 283
                    Err(_) => (e.content.len() as u64, 0),
232 284
                },
233 285
            };
234 -
            let hash = match &e.content_hash {
235 -
                Some(h) => h.clone(),
236 -
                None => blake3::hash(&e.content).to_hex().to_string(),
237 -
            };
238 -
            let blob = blobs.get(path).copied().or(e.blob);
239 286
            newest_ns = newest_ns.max(mtime_ns);
240 -
            body.push_str(&manifest_line(
241 -
                path,
287 +
            let entry = ManifestEntry {
242 288
                size,
243 289
                mtime_ns,
244 -
                &hash,
245 -
                blob,
246 -
                e.graph_key.as_deref(),
247 -
                e.exec,
248 -
            ));
249 -
        }
290 +
                hash: e
291 +
                    .content_hash
292 +
                    .unwrap_or_else(|| *blake3::hash(&e.content).as_bytes()),
293 +
                blob: blobs.get(path).copied().or(e.blob),
294 +
                graph: e.graph_key,
295 +
                exec: e.exec,
296 +
            };
297 +
            (path.as_str(), entry)
298 +
        });
299 +
        let new = encode_manifest(conflicted, uncommitted, written, &mut rows);
300 +
        drop(rows);
250 301
        let p = self.manifest_path();
251 302
        // Leave an unchanged manifest alone when its stamp already trusts
252 303
        // every entry (none is within the racy window): rewriting it would
253 304
        // change nothing but the file, which editors watching the tree
254 305
        // (and subrepos inside it) take as a change.
255 -
        if let Ok(old) = fs::read_to_string(&p) {
256 -
            let old_written: Option<u128> = old.lines().find_map(|l| {
257 -
                l.strip_prefix("# written=")
258 -
                    .and_then(|v| v.trim().parse().ok())
259 -
            });
260 -
            let old_rest: String = old
261 -
                .lines()
262 -
                .filter(|l| !l.starts_with("# written="))
263 -
                .map(|l| format!("{l}\n"))
264 -
                .collect();
265 -
            if let Some(w) = old_written {
266 -
                if old_rest == format!("{head}{body}") && newest_ns + 2_000_000_000 < w {
306 +
        if let Ok(old) = fs::read(&p) {
307 +
            let w = MANIFEST_WRITTEN;
308 +
            if old.len() == new.len()
309 +
                && old[..w.start] == new[..w.start]
310 +
                && old[w.end..] == new[w.end..]
311 +
            {
312 +
                let old_written = u128::from_le_bytes(old[w.clone()].try_into().unwrap());
313 +
                if newest_ns + 2_000_000_000 < old_written {
267 314
                    return Ok(());
268 315
                }
269 316
            }
270 317
        }
271 -
        let text = format!("{head}# written={written}\n{body}");
272 318
        fs::create_dir_all(p.parent().unwrap())?;
273 -
        write_atomic(&p, text.as_bytes())
319 +
        write_atomic(&p, &new)
274 320
    }
275 321
276 322
    /// Change the header flags, and set the graph key of each path in
…
3 unchanged lines
280 326
        &self,
281 327
        conflicted: bool,
282 328
        uncommitted: bool,
283 -
        keys: &HashMap<String, String>,
329 +
        keys: &HashMap<String, [u8; 32]>,
284 330
    ) -> Result<()> {
285 331
        self.manifest_cache.take();
286 332
        self.listing_cache.take();
287 333
        let p = self.manifest_path();
288 -
        let text = match fs::read_to_string(&p) {
289 -
            Ok(t) => t,
290 -
            Err(_) => return self.write_manifest(conflicted, uncommitted),
334 +
        let Some((_, _, m)) = fs::read(&p).ok().and_then(|b| decode_manifest(&b)) else {
335 +
            return self.write_manifest(conflicted, uncommitted);
291 336
        };
292 -
        let mut out = String::with_capacity(text.len());
293 -
        for line in text.lines() {
294 -
            if line.starts_with("# conflicted=") {
295 -
                out.push_str(&format!("# conflicted={}\n", conflicted as u8));
296 -
            } else if line.starts_with("# uncommitted=") {
297 -
                out.push_str(&format!("# uncommitted={}\n", uncommitted as u8));
298 -
            } else {
299 -
                let f: Vec<&str> = line.splitn(7, '\t').collect();
300 -
                match (f.len() == 7).then(|| keys.get(f[6])).flatten() {
301 -
                    Some(k) => out.push_str(&format!(
302 -
                        "{}\t{}\t{}\t{}\t{k}\t{}\t{}\n",
303 -
                        f[0], f[1], f[2], f[3], f[5], f[6]
304 -
                    )),
305 -
                    None => {
306 -
                        out.push_str(line);
307 -
                        out.push('\n');
308 -
                    }
309 -
                }
337 +
        let mut rows = m.entries.iter().map(|(path, e)| {
338 +
            let mut e = e.clone();
339 +
            if let Some(k) = keys.get(path) {
340 +
                e.graph = Some(*k);
310 341
            }
311 -
        }
312 -
        write_atomic(&p, out.as_bytes())
342 +
            (path.as_str(), e)
343 +
        });
344 +
        write_atomic(
345 +
            &p,
346 +
            &encode_manifest(conflicted, uncommitted, m.written_ns, &mut rows),
347 +
        )
313 348
    }
314 349
315 350
    /// Forget the manifest after an operation that changed the working-copy
…
34 unchanged lines
350 385
    }
351 386
352 387
    fn parse_manifest(&self, path: &std::path::Path) -> Result<Option<(bool, bool, Manifest)>> {
353 -
        let text = match fs::read_to_string(path) {
354 -
            Ok(t) => t,
388 +
        let buf = match fs::read(path) {
389 +
            Ok(b) => b,
355 390
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
356 391
            Err(e) => return Err(e.into()),
357 392
        };
358 -
        let mut out = Manifest::default();
359 -
        let mut conflicted = false;
360 -
        let mut uncommitted = false;
361 -
        for line in text.lines() {
362 -
            if let Some(v) = line.strip_prefix("# conflicted=") {
363 -
                conflicted = v.trim() == "1";
364 -
                continue;
365 -
            }
366 -
            if let Some(v) = line.strip_prefix("# uncommitted=") {
367 -
                uncommitted = v.trim() == "1";
368 -
                continue;
369 -
            }
370 -
            if let Some(v) = line.strip_prefix("# written=") {
371 -
                out.written_ns = v.trim().parse().unwrap_or(u128::MAX);
372 -
                continue;
373 -
            }
374 -
            // A line of another shape (an older manifest) is no entry: the
375 -
            // file is then read, and the next snapshot rewrites the line.
376 -
            let f: Vec<&str> = line.splitn(7, '\t').collect();
377 -
            if f.len() == 7 {
378 -
                out.entries.insert(
379 -
                    f[6].to_string(),
380 -
                    ManifestEntry {
381 -
                        size: f[0].parse().unwrap_or(0),
382 -
                        mtime_ns: f[1].parse().unwrap_or(0),
383 -
                        hash: f[2].to_string(),
384 -
                        blob: ObjectRef::from_hex(f[3]),
385 -
                        graph: (f[4] != "-").then(|| f[4].to_string()),
386 -
                        exec: f[5] == "x",
387 -
                    },
388 -
                );
389 -
            }
390 -
        }
391 -
        Ok(Some((conflicted, uncommitted, out)))
393 +
        Ok(decode_manifest(&buf))
392 394
    }
393 395
394 396
    /// Whether the working copy is certainly clean, known from the manifest
…
18 unchanged lines
413 415
            let path = self.root.join(rel);
414 416
            let same = match fs::symlink_metadata(&path) {
415 417
                Ok(md) if md.file_type().is_symlink() => fs::read_link(&path).is_ok_and(|t| {
416 -
                    blake3::hash(t.to_string_lossy().as_bytes())
417 -
                        .to_hex()
418 -
                        .as_str()
419 -
                        == e.hash
418 +
                    *blake3::hash(t.to_string_lossy().as_bytes()).as_bytes() == e.hash
420 419
                }),
421 420
                Ok(md) if md.is_file() => {
422 421
                    md.len() == e.size
423 -
                        && fs::read(&path)
424 -
                            .is_ok_and(|b| blake3::hash(&b).to_hex().as_str() == e.hash)
422 +
                        && fs::read(&path).is_ok_and(|b| *blake3::hash(&b).as_bytes() == e.hash)
425 423
                }
426 424
                _ => false,
427 425
            };
…
36 unchanged lines
464 462
                manifest
465 463
                    .entries
466 464
                    .get(rel)
467 -
                    .is_none_or(|me| blake3::hash(target).to_hex().as_str() != me.hash)
465 +
                    .is_none_or(|me| *blake3::hash(target).as_bytes() != me.hash)
468 466
            });
469 467
            if links_differ {
470 468
                return Ok(true);
…
6 unchanged lines
477 475
                        || super::is_executable(m) != me.exec
478 476
                        || (mtime_ns(m) != me.mtime_ns
479 477
                            && !fs::read(path)
480 -
                                .is_ok_and(|b| blake3::hash(&b).to_hex().as_str() == me.hash))
478 +
                                .is_ok_and(|b| *blake3::hash(&b).as_bytes() == me.hash))
481 479
                })
482 480
            });
483 481
            if files_differ {
…
16 unchanged lines
500 498
                if m.len() != me.size
501 499
                    || super::is_executable(&m) != me.exec
502 500
                    || (mtime_ns(&m) != me.mtime_ns
503 -
                        && blake3::hash(&fs::read(&path)?).to_hex().as_str() != me.hash)
501 +
                        && *blake3::hash(&fs::read(&path)?).as_bytes() != me.hash)
504 502
                {
505 503
                    return Ok(true);
506 504
                }
…
85 unchanged lines
modified crates/oxy-core/src/repo/snapshot.rs +6 −9
…
332 unchanged lines
333 333
            t
334 334
        };
335 335
        // A text file's graph key, worked out when the walk asks for it.
336 -
        let key_of = |p: &str| -> Option<String> {
336 +
        let key_of = |p: &str| -> Option<[u8; 32]> {
337 337
            let e = view.entries.get(p)?;
338 338
            if e.kind != NodeKind::File || e.mode.opaque {
339 339
                return None;
340 340
            }
341 -
            Some(crate::hash::ObjectRef(base.graph_key(&e.node)?).to_hex())
341 +
            base.graph_key(&e.node)
342 342
        };
343 343
        let mut fs_entries = {
344 344
            let _t = crate::timing::phase("snapshot: read working copy");
…
1 unchanged lines
346 346
        };
347 347
        // The keys of the files read, before building graphs drops them: a
348 348
        // file found equal to its base rendering keeps its key.
349 -
        let keys: HashMap<String, String> = fs_entries
349 +
        let keys: HashMap<String, [u8; 32]> = fs_entries
350 350
            .iter()
351 351
            .filter(|(_, e)| !e.unchanged && e.kind == NodeKind::File)
352 352
            .filter_map(|(p, _)| Some((p.clone(), key_of(p)?)))
…
45 unchanged lines
398 398
                        bytes,
399 399
                        materialized,
400 400
                    }) if !materialized.is_conflicted() && *bytes == *e.content => {
401 -
                        keys.get(p).cloned()
401 +
                        keys.get(p).copied()
402 402
                    }
403 403
                    _ => None,
404 404
                };
…
195 unchanged lines
600 600
    /// The graph key of each text file of the state at `heads`, by path,
601 601
    /// from a history-only load that builds no graph. Files with an `eol`
602 602
    /// attribute are left out: their bytes on disk are not the rendering.
603 -
    fn committed_graph_keys(&self, st: &State) -> HashMap<String, String> {
603 +
    fn committed_graph_keys(&self, st: &State) -> HashMap<String, [u8; 32]> {
604 604
        let _t = crate::timing::phase("commit: graph keys");
605 605
        let attrs = self.disk_attributes();
606 606
        st.tree()
…
3 unchanged lines
610 610
            .filter(|(p, e)| {
611 611
                e.kind == NodeKind::File && !e.mode.opaque && attrs.lookup(p).eol.is_none()
612 612
            })
613 -
            .filter_map(|(p, e)| {
614 -
                let k = st.graph_key(&e.node)?;
615 -
                Some((p.clone(), crate::hash::ObjectRef(k).to_hex()))
616 -
            })
613 +
            .filter_map(|(p, e)| Some((p.clone(), st.graph_key(&e.node)?)))
617 614
            .collect()
618 615
    }
619 616
…
1934 unchanged lines