OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb / scripts / gen-modules-manifest.js

153 lines · javascript ref:
#!/usr/bin/env node
/**
 * gen-modules-manifest.js — regenerate hyper-fleeky/modules.json
 *
 * Scans hyper-fleeky/ for git repos and records where each one lives, which
 * bare repo backs it, and what it currently is. The manifest is GENERATED —
 * never hand-edit it, re-run this instead. Hand-maintenance is why the
 * predecessors (projects.json, dependencies.json, sync-submodules.sh) rotted
 * into describing a layout that no longer exists.
 *
 * Deliberately records NO commit SHAs. Pins in this repo have never been
 * maintained (pear-git drifted 108 commits while submodule tooling was
 * broken), and the workspace consumes these modules by PATH via `file:..`
 * deps, not by pin — so a SHA here would be maintenance burden buying
 * nothing. Modules tracked as real submodules keep their pins in git, and
 * are marked "submodule" below.
 *
 *   node scripts/gen-modules-manifest.js            # write manifest
 *   node scripts/gen-modules-manifest.js --check    # exit 1 if stale (CI)
 */

const fs = require('fs')
const path = require('path')
const { execFileSync } = require('child_process')

const ROOT = path.resolve(__dirname, '..')
const SCAN = path.join(ROOT, 'hyper-fleeky')
const BARES = '/home/fleeky/git-repos'
const OUT = path.join(SCAN, 'modules.json')

function git (cwd, args) {
  try {
    return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
  } catch {
    return null
  }
}

function isRepo (dir) {
  return fs.existsSync(path.join(dir, '.git'))
}

// Paths the parent repo records as gitlinks — these keep pins in git and are
// not the manifest's business, but we list them so the manifest is a complete
// map rather than a partial one (partial tracking is what created the mess).
function submodulePaths () {
  const out = git(ROOT, ['ls-files', '-s', 'hyper-fleeky'])
  if (!out) return new Set()
  return new Set(
    out.split('\n')
      .filter(l => l.startsWith('160000'))
      .map(l => l.split('\t')[1])
  )
}

// A name-matched bare repo is NOT proof of the right target: packages/pgh
// pinned a commit absent from pgh.git but present in pgh-legacy.git, and
// hypernote is backed by hyper-note.git. Only claim a backing repo when the
// module's current HEAD actually resolves inside it.
function findBare (name, dir) {
  const head = git(dir, ['rev-parse', 'HEAD'])
  const origin = git(dir, ['remote', 'get-url', 'origin'])
  const candidates = []
  if (origin) candidates.push(origin)
  candidates.push(path.join(BARES, name + '.git'))
  candidates.push(path.join(BARES, name.replace(/^hyper-?/, 'hyper-') + '.git'))
  // hypernote -> hyper-note.git style split
  if (/^hyper[a-z]/.test(name)) candidates.push(path.join(BARES, name.replace(/^hyper/, 'hyper-') + '.git'))

  const seen = new Set()
  let fallback = null
  for (const c of candidates) {
    if (!c || seen.has(c)) continue
    seen.add(c)
    if (!fs.existsSync(c)) continue
    if (!head) return { repo: c, verified: false }
    const ok = git(c, ['cat-file', '-e', head + '^{commit}']) !== null
    if (ok) return { repo: c, verified: true }
    // Bare exists but does not hold our HEAD => unpushed work, not a missing
    // repo. Keep it as the best-known target so the report can say so.
    if (!fallback) fallback = c
  }
  return { repo: fallback, verified: false }
}

function version (dir) {
  try {
    return JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).version || null
  } catch {
    return null
  }
}

function scan () {
  const subs = submodulePaths()
  const modules = {}

  for (const name of fs.readdirSync(SCAN).sort()) {
    const dir = path.join(SCAN, name)
    let st
    try { st = fs.lstatSync(dir) } catch { continue }
    if (st.isSymbolicLink() || !st.isDirectory()) continue
    if (!isRepo(dir)) continue

    const rel = 'hyper-fleeky/' + name
    const { repo, verified } = findBare(name, dir)

    modules[name] = {
      path: rel,
      tracking: subs.has(rel) ? 'submodule' : 'manifest',
      branch: git(dir, ['rev-parse', '--abbrev-ref', 'HEAD']),
      version: version(dir),
      repo,
      // false => the backing repo does not contain this module's HEAD, i.e.
      // there is unpushed local work. Treat as "not safely reproducible yet".
      backed: verified,
      lastCommit: git(dir, ['log', '-1', '--format=%ad', '--date=short'])
    }
  }
  return modules
}

const modules = scan()
const manifest = {
  _comment: 'GENERATED by scripts/gen-modules-manifest.js — do not hand-edit; re-run the generator.',
  _supersedes: ['hyper-fleeky/projects.json', 'hyper-fleeky/dependencies.json', 'sync-submodules.sh'],
  generatedFrom: 'hyper-fleeky/',
  count: Object.keys(modules).length,
  modules
}

const json = JSON.stringify(manifest, null, 2) + '\n'

if (process.argv.includes('--check')) {
  const current = fs.existsSync(OUT) ? fs.readFileSync(OUT, 'utf8') : ''
  if (current !== json) {
    console.error('modules.json is STALE — run: node scripts/gen-modules-manifest.js')
    process.exit(1)
  }
  console.log('modules.json up to date (' + manifest.count + ' modules)')
} else {
  fs.writeFileSync(OUT, json)
  const unbacked = Object.entries(modules).filter(([, m]) => !m.backed)
  console.log('wrote ' + OUT + ' (' + manifest.count + ' modules)')
  if (unbacked.length) {
    console.log('\nWARNING — these modules\' HEAD is not in any backing repo:')
    for (const [n, m] of unbacked) {
      console.log('  ' + n + (m.repo ? '  UNPUSHED -> ' + m.repo : '  NO BARE REPO FOUND'))
    }
    console.log('\nUnpushed work exists only on this disk, and cannot be pinned or synced.')
  }
}