OXYGENHUB

gc_keeping: keep the blobs a host names beside what history reaches 9d580f2132f3

A forge stores its job artifacts as blobs in a repository's store and
names them only in its own database; gc found them unreachable and
pruned them once the grace period passed. Repo::gc_keeping takes extra
blobs and keeps each with its chunks; gc_with is gc_keeping with none.

refs andymccall/oxygenhub#126
Author
Andy McCall @andymccall <[email protected]>
When
· 2026-10-10 23:08 UTC
Signature
✔ signed by Andy McCall
Hash
9d580f2132f3c72aa1747f36ee4d1599236825c7048422e7abd4e9a69f38f0fa
Depends on
34fad4a92451 75167672ebcd 8fd78eb52ad2 982864068a59 9ae4d15a0e4b ba160f60219e
Depended on by
767ac6cd0545 530a0fc650a2
In bookmarks
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.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
5 atoms, 0 tree operations, 3 files

Browse the tree at this change

modified CHANGELOG.md +2 −0
…
1 unchanged lines
2 2
3 3
## Unreleased
4 4
5 +
- **gc can keep what a host names (oxygenhub#126).** `Repo::gc_keeping` keeps extra blobs and their chunks beside what history reaches, so a forge's job artifacts, referenced only from its own database, survive garbage collection.
6 +
5 7
- **`oxy remote remove` removes all of a remote (#86).** It blanked the URL alone: the token, CA and store settings stayed in the config and the remote's tracking bookmarks in the view, read later as if current. Now every `remote.NAME.*` key goes, with its tracking bookmarks and a store remote's mirror; an unknown name is refused.
6 8
7 9
## 0.4.3 (2026-10-10)
…
166 unchanged lines
modified crates/oxy-core/src/repo/gc.rs +24 −1
…
30 unchanged lines
31 31
    /// [`Repo::gc`], reporting each phase and the packing's progress
32 32
    /// (oxygen#66), for a spinner or a log.
33 33
    pub fn gc_with(&self, now: bool, progress: crate::store::Progress) -> Result<GcReport> {
34 +
        self.gc_keeping(now, &[], progress)
35 +
    }
36 +
37 +
    /// [`Repo::gc_with`], keeping `extra` blobs and their chunks as well:
38 +
    /// objects a host stored for itself and names from outside the
39 +
    /// repository's history, such as a forge's job artifacts
40 +
    /// (oxygenhub#126).
41 +
    pub fn gc_keeping(
42 +
        &self,
43 +
        now: bool,
44 +
        extra: &[ObjectRef],
45 +
        progress: crate::store::Progress,
46 +
    ) -> Result<GcReport> {
34 47
        progress("finding what is reachable");
35 -
        let keep = self.reachable()?;
48 +
        let mut keep = self.reachable()?;
49 +
        for r in extra {
50 +
            if !self.store.has(r) || !keep.insert(*r) {
51 +
                continue;
52 +
            }
53 +
            if let Ok(body) = self.store.get_typed(r, ObjectType::Blob) {
54 +
                if let Ok(blob) = Blob::decode(&body) {
55 +
                    keep.extend(blob.chunks);
56 +
                }
57 +
            }
58 +
        }
36 59
        let grace = if now {
37 60
            std::time::Duration::ZERO
38 61
        } else {
…
143 unchanged lines
modified crates/oxy-core/src/repo/tests.rs +23 −0
…
8296 unchanged lines
8297 8297
        "a second removal is refused"
8298 8298
    );
8299 8299
}
8300 +
8301 +
/// oxygenhub#126: a blob a host stored for itself (a job's artifact) is
8302 +
/// unreachable from history, so gc prunes it, unless the host names it:
8303 +
/// then it and its chunks stay, and it reads back whole.
8304 +
#[test]
8305 +
fn gc_keeps_the_blobs_a_host_names() {
8306 +
    hermetic();
8307 +
    let dir = tempfile::tempdir().unwrap();
8308 +
    let repo = Repo::init(dir.path()).unwrap();
8309 +
    write(dir.path(), "a.txt", b"a\n");
8310 +
    repo.commit("a", &[]).unwrap();
8311 +
    // Large enough to be several chunks.
8312 +
    let body: Vec<u8> = (0..400_000u32)
8313 +
        .flat_map(|i| (i.wrapping_mul(2654435761)).to_le_bytes())
8314 +
        .collect();
8315 +
    let kept = repo.store_blob(&body).unwrap();
8316 +
    let lost = repo.store_blob(b"an artifact nobody names").unwrap();
8317 +
    repo.gc_keeping(true, &[kept], &mut |_| {}).unwrap();
8318 +
    assert_eq!(repo.read_blob(&kept).unwrap(), body, "named: kept whole");
8319 +
    assert!(!repo.store().has(&lost), "not named: pruned");
8320 +
    repo.gc(true).unwrap();
8321 +
    assert!(!repo.store().has(&kept), "a plain gc prunes it");
8322 +
}