OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / test / unit / ref-reader.test.js

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