OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / git-bundle.test.js

123 lines · javascript ref:
// gip-overlay P3 (transport foundation) — git-bundle delta create/apply.
//
// Objects move between nodes as `git bundle` deltas: the producer bundles
// the objects a push added (oldOid..newOid), the fetcher unbundles them into
// the local .git. A delta bundle carries its prerequisite (oldOid), so
// unbundling fails cleanly when the base objects are absent — that failure is
// what drives the bundle CHAIN (fetch the prerequisite bundle first). Real
// git, no mocks.

import test from 'brittle'
import { mkdtempSync, writeFileSync } from '#node:fs'
import { join } from '#node:path'
import { tmpdir } from '#node:os'

import { execFileSync } from '../lib/_compat-exec.js'
import { createDeltaBundle, applyBundle } from '../lib/git-bundle.js'
import { ZERO_OID } from 'hyper-issues'

function git (args, opts = {}) {
  return execFileSync('git', args, { encoding: 'utf8', ...opts }).toString().trim()
}

// origin bare repo with three commits on main (o1 -> o2 -> o3).
function seedOrigin () {
  const dir = mkdtempSync(join(tmpdir(), 'pgv3-bundle-'))
  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] }
}

function emptyBare () {
  const bare = join(mkdtempSync(join(tmpdir(), 'pgv3-bundle-t-')), 't.git')
  git(['init', '--bare', '-q', bare])
  return bare
}

function has (bare, oid) {
  try { git(['--git-dir', bare, 'cat-file', '-e', oid + '^{object}']); return true }
  catch { return false }
}

test('creation bundle (oldOid = ZERO) carries full history; unbundles into an empty repo', async (t) => {
  const o = seedOrigin()
  const out = join(o.dir, 'create.bundle')

  const res = await createDeltaBundle({ repoPath: o.bare, ref: 'refs/heads/main', oldOid: ZERO_OID, newOid: o.o1, outPath: out })
  t.is(res.path, out, 'returns the bundle path')

  const target = emptyBare()
  await applyBundle({ repoPath: target, bundlePath: out })
  t.ok(has(target, o.o1), 'o1 objects present after unbundle')
})

test('delta bundle (oldOid..newOid) unbundles when prerequisite is present', async (t) => {
  const o = seedOrigin()
  const seed = join(o.dir, 'seed.bundle')
  const delta = join(o.dir, 'delta.bundle')

  await createDeltaBundle({ repoPath: o.bare, ref: 'refs/heads/main', oldOid: ZERO_OID, newOid: o.o1, outPath: seed })
  await createDeltaBundle({ repoPath: o.bare, ref: 'refs/heads/main', oldOid: o.o1, newOid: o.o2, outPath: delta })

  const target = emptyBare()
  await applyBundle({ repoPath: target, bundlePath: seed })   // o1
  t.ok(has(target, o.o1))
  await applyBundle({ repoPath: target, bundlePath: delta })  // o1 -> o2
  t.ok(has(target, o.o2), 'o2 present after delta unbundle')
})

test('delta bundle fails to unbundle when the prerequisite is missing (drives the chain)', async (t) => {
  const o = seedOrigin()
  const delta = join(o.dir, 'delta.bundle')
  await createDeltaBundle({ repoPath: o.bare, ref: 'refs/heads/main', oldOid: o.o1, newOid: o.o2, outPath: delta })

  const fresh = emptyBare() // lacks o1
  await t.exception(applyBundle({ repoPath: fresh, bundlePath: delta }), /prerequisite|lacks|missing/i)
  t.absent(has(fresh, o.o2), 'no objects materialized from an unsatisfiable delta')
})

test('createDeltaBundle pins the exact newOid even when the ref has moved past it', async (t) => {
  const o = seedOrigin() // ref now at o3
  const delta = join(o.dir, 'pinned.bundle')

  // Bundle o1..o2 although refs/heads/main is at o3.
  await createDeltaBundle({ repoPath: o.bare, ref: 'refs/heads/main', oldOid: o.o1, newOid: o.o2, outPath: delta })

  const target = emptyBare()
  await createDeltaBundle({ repoPath: o.bare, ref: 'refs/heads/main', oldOid: ZERO_OID, newOid: o.o1, outPath: join(o.dir, 's.bundle') })
  await applyBundle({ repoPath: target, bundlePath: join(o.dir, 's.bundle') })
  await applyBundle({ repoPath: target, bundlePath: delta })

  t.ok(has(target, o.o2), 'o2 present')
  t.absent(has(target, o.o3), 'o3 NOT pulled in — bundle pinned o2, not the moved ref')
})

test('createDeltaBundle returns null for an empty delta (oldOid === newOid)', async (t) => {
  const o = seedOrigin()
  const res = await createDeltaBundle({ repoPath: o.bare, ref: 'refs/heads/main', oldOid: o.o2, newOid: o.o2, outPath: join(o.dir, 'noop.bundle') })
  t.is(res, null, 'no bundle for a no-op ref move')
})

test('bundlePrerequisites reports the base oids a delta needs', async (t) => {
  const o = seedOrigin()
  const delta = join(o.dir, 'delta.bundle')
  await createDeltaBundle({ repoPath: o.bare, ref: 'refs/heads/main', oldOid: o.o1, newOid: o.o2, outPath: delta })

  const { bundlePrerequisites } = await import('../lib/git-bundle.js')
  const prereqs = bundlePrerequisites({ repoPath: o.bare, bundlePath: delta })
  t.ok(prereqs.includes(o.o1), 'delta declares o1 as prerequisite')
})