// gip-overlay P3c — the fetcher (ensureObjects).
//
// Given a target oid, resolve the bundle chain from the autobase (oid ->
// baseOid back to genesis or to an oid already present locally), fetch each
// delta bundle from the overlay, and unbundle them base-first into the bare
// repo. This is the ensureObjects() RefReconciler (P2) calls. Real git + a
// real HyperIssues tracker; the overlay is faked (content-hash -> bundle
// file) so this is decoupled from the swarm (the real overlay is the P3e e2e).
import test from 'brittle'
import Corestore from 'corestore'
import { mkdtempSync, writeFileSync } from '#node:fs'
import { join } from '#node:path'
import { tmpdir } from '#node:os'
import { HyperIssues, ZERO_OID } from 'hyper-issues'
import { execFileSync } from '../lib/_compat-exec.js'
import { createDeltaBundle } from '../lib/git-bundle.js'
import { materializeObject, makeOverlayEnsureObjects } from '../lib/git-overlay-fetch.js'
function git (args, opts = {}) {
return execFileSync('git', args, { encoding: 'utf8', ...opts }).toString().trim()
}
function has (bare, oid) {
try { git(['--git-dir', bare, 'cat-file', '-e', oid + '^{object}']); return true } catch { return false }
}
function emptyBare () {
const bare = join(mkdtempSync(join(tmpdir(), 'pgv3-fetch-t-')), 't.git')
git(['init', '--bare', '-q', bare])
return bare
}
// Origin with o1->o2->o3 and a delta bundle per step on disk. Returns a fake
// overlay whose fetchFile(hash) serves the matching bundle file, plus the
// recorded (oid, baseOid, contentHash) chain.
function seedChain () {
const dir = mkdtempSync(join(tmpdir(), 'pgv3-fetch-'))
const bare = join(dir, 'o.git')
const work = join(dir, 'work')
git(['init', '--bare', '-q', bare])
git(['init', '-q', work])
git(['-C', work, 'config', 'user.email', 't@t'])
git(['-C', work, 'config', 'user.name', 'T'])
git(['-C', work, 'config', 'commit.gpgsign', 'false'])
const oids = []
for (const body of ['one\n', 'two\n', 'three\n']) {
writeFileSync(join(work, 'a.txt'), body)
git(['-C', work, 'add', '.'])
git(['-C', work, 'commit', '-q', '-m', body.trim()])
oids.push(git(['-C', work, 'rev-parse', 'HEAD']))
}
git(['-C', work, 'push', '-q', bare, 'HEAD:refs/heads/main'])
return { dir, bare, work, o1: oids[0], o2: oids[1], o3: oids[2] }
}
async function makeTracker (t) {
const store = new Corestore(mkdtempSync(join(tmpdir(), 'pgv3-fetch-store-')))
const tracker = new HyperIssues(store, { namespace: 'issues:repo:r' })
await tracker.ready()
t.teardown(async () => { try { await tracker.close() } catch {}; try { await store.close() } catch {} })
return tracker
}
function fakeOverlay (map) {
const calls = []
return {
calls,
async fetchFile (hash) {
calls.push(hash)
const p = map.get(hash)
return p ? { data: null, destPath: p, local: false } : null
}
}
}
// Build the three bundles on disk and register the chain in the tracker.
async function buildChain (s, tracker) {
const seed = join(s.dir, 'seed.bundle')
const d12 = join(s.dir, 'd12.bundle')
const d23 = join(s.dir, 'd23.bundle')
await createDeltaBundle({ repoPath: s.bare, ref: 'refs/heads/main', oldOid: ZERO_OID, newOid: s.o1, outPath: seed })
await createDeltaBundle({ repoPath: s.bare, ref: 'refs/heads/main', oldOid: s.o1, newOid: s.o2, outPath: d12 })
await createDeltaBundle({ repoPath: s.bare, ref: 'refs/heads/main', oldOid: s.o2, newOid: s.o3, outPath: d23 })
await tracker.addBundle({ oid: s.o1, baseOid: ZERO_OID, contentHash: 'h1' }, 'u')
await tracker.addBundle({ oid: s.o2, baseOid: s.o1, contentHash: 'h2' }, 'u')
await tracker.addBundle({ oid: s.o3, baseOid: s.o2, contentHash: 'h3' }, 'u')
const overlay = fakeOverlay(new Map([['h1', seed], ['h2', d12], ['h3', d23]]))
return overlay
}
test('materializes a target oid by fetching+applying the whole chain', async (t) => {
const s = seedChain()
const tracker = await makeTracker(t)
const overlay = await buildChain(s, tracker)
const target = emptyBare()
await materializeObject({ oid: s.o3, tracker, overlay, repoPath: target })
t.ok(has(target, s.o3), 'o3 present')
t.ok(has(target, s.o1), 'o1 present (whole chain applied)')
t.alike(overlay.calls, ['h1', 'h2', 'h3'], 'fetched base-first, full chain')
})
test('stops the chain walk at an oid already present locally', async (t) => {
const s = seedChain()
const tracker = await makeTracker(t)
const overlay = await buildChain(s, tracker)
const target = emptyBare()
// pre-seed o1 so the walk should stop there and fetch only h2, h3
await materializeObject({ oid: s.o1, tracker, overlay, repoPath: target })
overlay.calls.length = 0
await materializeObject({ oid: s.o3, tracker, overlay, repoPath: target })
t.ok(has(target, s.o3), 'o3 present')
t.alike(overlay.calls, ['h2', 'h3'], 'only the missing tail fetched')
})
test('no-op when the object is already present', async (t) => {
const s = seedChain()
const tracker = await makeTracker(t)
const overlay = await buildChain(s, tracker)
const target = emptyBare()
await materializeObject({ oid: s.o1, tracker, overlay, repoPath: target })
overlay.calls.length = 0
await materializeObject({ oid: s.o1, tracker, overlay, repoPath: target })
t.alike(overlay.calls, [], 'no fetch for an object already in the repo')
})
test('throws when a chain link is missing from the autobase', async (t) => {
const s = seedChain()
const tracker = await makeTracker(t)
const target = emptyBare()
// record only o3 -> o2, but never record o2's bundle
const d23 = join(s.dir, 'd23.bundle')
await createDeltaBundle({ repoPath: s.bare, ref: 'refs/heads/main', oldOid: s.o2, newOid: s.o3, outPath: d23 })
await tracker.addBundle({ oid: s.o3, baseOid: s.o2, contentHash: 'h3' }, 'u')
const overlay = fakeOverlay(new Map([['h3', d23]]))
await t.exception(materializeObject({ oid: s.o3, tracker, overlay, repoPath: target }), /no bundle recorded for/i)
})
test('throws when the overlay cannot serve a bundle', async (t) => {
const s = seedChain()
const tracker = await makeTracker(t)
await buildChain(s, tracker) // records h1/h2/h3 in tracker
const target = emptyBare()
const emptyOverlay = fakeOverlay(new Map()) // serves nothing
await t.exception(materializeObject({ oid: s.o1, tracker, overlay: emptyOverlay, repoPath: target }), /overlay fetch failed/i)
})
test('makeOverlayEnsureObjects wires the RefReconciler ensureObjects(oids) shape', async (t) => {
const s = seedChain()
const tracker = await makeTracker(t)
const overlay = await buildChain(s, tracker)
const target = emptyBare()
const ensureObjects = makeOverlayEnsureObjects({ tracker, overlay, repoPath: target })
await ensureObjects([s.o2, s.o3])
t.ok(has(target, s.o2) && has(target, s.o3), 'both requested oids materialized')
})