/**
* git-bundle — gip-overlay P3 object transport (the git half).
*
* 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 bare repo. `git` does all the git work on both ends (no JS git engine)
* — these helpers just shell out via the cross-runtime exec shim.
*
* A delta bundle records its prerequisite (oldOid) in its header, so:
* - unbundling fails cleanly when the base objects are absent, which is what
* drives the bundle CHAIN (fetch the prerequisite's bundle first), and
* - `bundlePrerequisites()` lets the fetcher know what a delta needs before
* attempting to apply it.
*
* `git bundle create` requires a REF (a bare sha is "Refusing to create empty
* bundle"), so we pin the exact newOid behind a throwaway ref — that also
* stops a concurrent push from widening the delta to the ref's latest tip.
*/
import fs from '#node:fs'
import { execFileSync } from './_compat-exec.js'
import { ZERO_OID } from 'hyper-issues'
function isZero (oid) { return !oid || oid === ZERO_OID }
/**
* Bundle the objects reachable from newOid but not from oldOid.
* @param {object} o
* o.repoPath bare repo (--git-dir)
* o.ref the ref being advanced (for the temp-ref label only)
* o.oldOid previous oid; ZERO/null ⇒ full history from newOid
* o.newOid target oid to bundle up to
* o.outPath where to write the .bundle
* o.git exec shim (injectable; defaults to execFileSync)
* @returns {Promise<{ path: string } | null>} null for an empty delta (no-op move)
*/
export async function createDeltaBundle (o = {}) {
const { repoPath, oldOid, newOid, outPath } = o
const git = o.git || execFileSync
if (!repoPath) throw new Error('createDeltaBundle requires repoPath')
if (!newOid) throw new Error('createDeltaBundle requires newOid')
if (newOid === oldOid) return null // nothing added
// Pin newOid behind a throwaway ref: gives `bundle create` a ref to record
// and freezes the tip against concurrent pushes.
const tmpRef = 'refs/gip-overlay/bundle-tmp-' + newOid
git('git', ['--git-dir', repoPath, 'update-ref', tmpRef, newOid], { encoding: 'utf8' })
try {
const args = ['--git-dir', repoPath, 'bundle', 'create', outPath, tmpRef]
if (!isZero(oldOid)) args.push('--not', oldOid)
git('git', args, { encoding: 'utf8' })
} finally {
try { git('git', ['--git-dir', repoPath, 'update-ref', '-d', tmpRef], { encoding: 'utf8' }) } catch {}
}
return { path: outPath }
}
/**
* Unbundle a bundle's objects into a bare repo. Throws (message carries git's
* stderr) if a delta's prerequisite objects are absent — the caller resolves
* the chain and retries.
* @returns {Promise<{ tips: Array<{ oid, ref }> }>}
*/
export async function applyBundle (o = {}) {
const { repoPath, bundlePath } = o
const git = o.git || execFileSync
if (!repoPath) throw new Error('applyBundle requires repoPath')
if (!bundlePath) throw new Error('applyBundle requires bundlePath')
let out
try {
out = git('git', ['--git-dir', repoPath, 'bundle', 'unbundle', bundlePath], { encoding: 'utf8' }).toString()
} catch (err) {
const detail = (err.stderr ? err.stderr.toString() : '') || err.message
const e = new Error('bundle unbundle failed: ' + detail.trim())
e.cause = err
throw e
}
// unbundle prints "<oid> <ref>" per tip
const tips = []
for (const line of out.split('\n')) {
const m = line.trim().match(/^([0-9a-f]{40,64})\s+(\S+)/)
if (m) tips.push({ oid: m[1], ref: m[2] })
}
return { tips }
}
/**
* Read the prerequisite oids a bundle requires (the `-<sha>` header lines).
* Empty array for a self-contained (creation) bundle.
* @returns {string[]}
*/
export function bundlePrerequisites (o = {}) {
const { bundlePath } = o
if (!bundlePath) throw new Error('bundlePrerequisites requires bundlePath')
const fd = fs.openSync(bundlePath, 'r')
try {
const buf = Buffer.alloc(65536)
const n = fs.readSync(fd, buf, 0, buf.length, 0)
const text = buf.subarray(0, n).toString('latin1')
const prereqs = []
for (const line of text.split('\n')) {
if (line === '') break // blank line ends the header (PACK follows)
if (line[0] === '-') {
const m = line.slice(1).match(/^([0-9a-f]{40,64})\b/)
if (m) prereqs.push(m[1])
}
}
return prereqs
} finally {
fs.closeSync(fd)
}
}