import test from 'brittle'
import fs from 'bare-fs'
import path from 'bare-path'
import os from 'bare-os'
import { readRefs } from '../../lib/git/ref-reader.js'
const tmpDir = path.join(os.tmpdir(), 'pear-git-test-ref-reader-' + Date.now())
// Helper: create a minimal bare git repo structure in a temp directory
async function createBareRepo (repoPath, opts = {}) {
await fs.promises.mkdir(repoPath, { recursive: true })
await fs.promises.mkdir(path.join(repoPath, 'refs', 'heads'), { recursive: true })
await fs.promises.mkdir(path.join(repoPath, 'refs', 'tags'), { recursive: true })
// Write HEAD — default is symbolic ref to main
const head = opts.head || 'ref: refs/heads/main\n'
await fs.promises.writeFile(path.join(repoPath, 'HEAD'), head)
// Write branch refs
if (opts.branches) {
for (const [name, sha] of Object.entries(opts.branches)) {
const refPath = path.join(repoPath, 'refs', 'heads', ...name.split('/'))
await fs.promises.mkdir(path.dirname(refPath), { recursive: true })
await fs.promises.writeFile(refPath, sha + '\n')
}
}
// Write tag refs
if (opts.tags) {
for (const [name, sha] of Object.entries(opts.tags)) {
const refPath = path.join(repoPath, 'refs', 'tags', ...name.split('/'))
await fs.promises.mkdir(path.dirname(refPath), { recursive: true })
await fs.promises.writeFile(refPath, sha + '\n')
}
}
// Write packed-refs file
if (opts.packedRefs) {
await fs.promises.writeFile(path.join(repoPath, 'packed-refs'), opts.packedRefs)
}
}
test('setup: create temp dir', async (t) => {
await fs.promises.mkdir(tmpDir, { recursive: true })
t.pass('temp dir created')
})
test('readRefs: reads branch refs from refs/heads/', async (t) => {
const repoPath = path.join(tmpDir, 'branches-repo')
await createBareRepo(repoPath, {
branches: {
main: 'aabbccdd00112233445566778899aabbccddeeff',
develop: '1122334455667788990011223344556677889900'
}
})
const refs = await readRefs(repoPath)
t.is(refs['refs/heads/main'], 'aabbccdd00112233445566778899aabbccddeeff')
t.is(refs['refs/heads/develop'], '1122334455667788990011223344556677889900')
})
test('readRefs: reads tag refs from refs/tags/', async (t) => {
const repoPath = path.join(tmpDir, 'tags-repo')
await createBareRepo(repoPath, {
branches: {
main: 'aabbccdd00112233445566778899aabbccddeeff'
},
tags: {
'v1.0.0': 'ff00112233445566778899aabbccddeeff001122',
'v2.0.0': 'ee00112233445566778899aabbccddeeff001133'
}
})
const refs = await readRefs(repoPath)
t.is(refs['refs/tags/v1.0.0'], 'ff00112233445566778899aabbccddeeff001122')
t.is(refs['refs/tags/v2.0.0'], 'ee00112233445566778899aabbccddeeff001133')
})
test('readRefs: reads HEAD as symbolic ref', async (t) => {
const repoPath = path.join(tmpDir, 'head-symbolic-repo')
await createBareRepo(repoPath, {
head: 'ref: refs/heads/main\n',
branches: {
main: 'aabbccdd00112233445566778899aabbccddeeff'
}
})
const refs = await readRefs(repoPath)
t.is(refs.HEAD, 'ref: refs/heads/main', 'HEAD is symbolic ref string')
})
test('readRefs: reads HEAD as detached hash', async (t) => {
const repoPath = path.join(tmpDir, 'head-detached-repo')
const sha = 'deadbeef00112233445566778899aabbccddeeff'
await createBareRepo(repoPath, {
head: sha + '\n'
})
const refs = await readRefs(repoPath)
t.is(refs.HEAD, sha, 'HEAD is raw SHA hash')
})
test('readRefs: handles empty repo (only HEAD, no branch/tag refs)', async (t) => {
const repoPath = path.join(tmpDir, 'empty-repo')
await createBareRepo(repoPath, {
head: 'ref: refs/heads/main\n'
// no branches, no tags
})
const refs = await readRefs(repoPath)
t.is(refs.HEAD, 'ref: refs/heads/main')
// Should only have HEAD — no branch or tag entries
const keys = Object.keys(refs).filter(k => k !== 'HEAD')
t.is(keys.length, 0, 'no ref entries besides HEAD')
})
test('readRefs: handles nested ref paths (refs/heads/feature/branch-name)', async (t) => {
const repoPath = path.join(tmpDir, 'nested-repo')
await createBareRepo(repoPath, {
branches: {
main: 'aabbccdd00112233445566778899aabbccddeeff',
'feature/login': '1111111111111111111111111111111111111111',
'feature/ui/dark-mode': '2222222222222222222222222222222222222222'
},
tags: {
'release/v1.0': '3333333333333333333333333333333333333333'
}
})
const refs = await readRefs(repoPath)
t.is(refs['refs/heads/main'], 'aabbccdd00112233445566778899aabbccddeeff')
t.is(refs['refs/heads/feature/login'], '1111111111111111111111111111111111111111')
t.is(refs['refs/heads/feature/ui/dark-mode'], '2222222222222222222222222222222222222222')
t.is(refs['refs/tags/release/v1.0'], '3333333333333333333333333333333333333333')
})
test('readRefs: returns all ref types together', async (t) => {
const repoPath = path.join(tmpDir, 'full-repo')
await createBareRepo(repoPath, {
head: 'ref: refs/heads/main\n',
branches: {
main: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
develop: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
},
tags: {
'v1.0.0': 'cccccccccccccccccccccccccccccccccccccccc'
}
})
const refs = await readRefs(repoPath)
t.is(Object.keys(refs).length, 4, 'HEAD + 2 branches + 1 tag')
t.ok(refs.HEAD, 'has HEAD')
t.ok(refs['refs/heads/main'], 'has main branch')
t.ok(refs['refs/heads/develop'], 'has develop branch')
t.ok(refs['refs/tags/v1.0.0'], 'has v1.0.0 tag')
})
test('readRefs: throws for nonexistent repo path', async (t) => {
const badPath = path.join(tmpDir, 'does-not-exist')
try {
await readRefs(badPath)
t.fail('should have thrown')
} catch (err) {
t.ok(err, 'throws error for missing repo')
}
})
test('readRefs: reads refs from packed-refs file', async (t) => {
const repoPath = path.join(tmpDir, 'packed-repo')
await createBareRepo(repoPath, {
packedRefs: [
'# pack-refs with: peeled fully-peeled sorted',
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa refs/heads/main',
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb refs/heads/develop',
'cccccccccccccccccccccccccccccccccccccccc refs/tags/v1.0.0',
''
].join('\n')
})
const refs = await readRefs(repoPath)
t.is(refs['refs/heads/main'], 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'packed branch main')
t.is(refs['refs/heads/develop'], 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'packed branch develop')
t.is(refs['refs/tags/v1.0.0'], 'cccccccccccccccccccccccccccccccccccccccc', 'packed tag v1.0.0')
})
test('readRefs: loose refs override packed-refs', async (t) => {
const repoPath = path.join(tmpDir, 'override-repo')
const packedSha = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
const looseSha = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
const packedOnlySha = 'cccccccccccccccccccccccccccccccccccccccc'
await createBareRepo(repoPath, {
branches: {
main: looseSha // loose ref — should win
},
packedRefs: [
packedSha + ' refs/heads/main', // will be overridden by loose
packedOnlySha + ' refs/heads/old-branch' // only in packed — should appear
].join('\n')
})
const refs = await readRefs(repoPath)
t.is(refs['refs/heads/main'], looseSha, 'loose ref overrides packed ref')
t.is(refs['refs/heads/old-branch'], packedOnlySha, 'packed-only ref is present')
})
test('readRefs: packed-refs with comments and peel markers are skipped', async (t) => {
const repoPath = path.join(tmpDir, 'peel-repo')
await createBareRepo(repoPath, {
packedRefs: [
'# pack-refs with: peeled fully-peeled sorted',
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa refs/tags/v1.0.0',
'^bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
'# another comment',
'cccccccccccccccccccccccccccccccccccccccc refs/heads/main',
''
].join('\n')
})
const refs = await readRefs(repoPath)
t.is(refs['refs/tags/v1.0.0'], 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'tag ref parsed')
t.is(refs['refs/heads/main'], 'cccccccccccccccccccccccccccccccccccccccc', 'branch ref parsed')
// Peel marker should NOT appear as a ref
const allKeys = Object.keys(refs)
const peelKeys = allKeys.filter(k => k.startsWith('^'))
t.is(peelKeys.length, 0, 'no peel marker refs')
})
test('readRefs: missing packed-refs file is handled gracefully', async (t) => {
// This test reuses the empty-repo scenario — no packed-refs file exists
const repoPath = path.join(tmpDir, 'no-packed-repo')
await createBareRepo(repoPath, {
branches: {
main: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
}
// no packedRefs — file should not exist
})
const refs = await readRefs(repoPath)
t.is(refs['refs/heads/main'], 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'loose ref works without packed-refs')
})
// --- Integration tests against real bare git repos ---
test('readRefs works on real bare git repo', async (t) => {
// Try to find a real repo in ~/git-repos/
const reposDir = path.join(os.homedir(), 'git-repos')
let repoPath = null
try {
const entries = await fs.promises.readdir(reposDir)
for (const e of entries) {
if (e.endsWith('.git')) { repoPath = path.join(reposDir, e); break }
}
} catch { }
if (!repoPath) { t.pass('skip: no bare repos in ~/git-repos/'); return }
const refs = await readRefs(repoPath)
t.ok(refs.HEAD, 'HEAD exists')
t.ok(Object.keys(refs).length > 1, 'has at least one branch ref')
for (const [ref, hash] of Object.entries(refs)) {
if (ref === 'HEAD') continue
t.is(hash.length, 40, ref + ' hash is 40 chars')
t.ok(/^[0-9a-f]{40}$/.test(hash), ref + ' hash is valid hex')
}
})
test('cleanup: remove temp dir', async (t) => {
await fs.promises.rm(tmpDir, { recursive: true, force: true })
t.pass('cleaned up')
})