OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / git-overlay-fetch.test.js

168 lines · javascript ref:
// 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')
})