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 | + | } |