current commit
260120.1 · feat: add support for stdin JSON input (#135)
#!/usr/bin/env node

import { exec } from 'node:child_process'
import fs from 'node:fs'
import http from 'node:http'
import https from 'node:https'
import path from 'node:path'
import { parseArgs } from 'node:util'
import pretty from '../lib/format-pretty.js'
import formatTap from '../lib/format-tap.js'
import Validator from '../lib/validator.js'
import Tap from '../lib/tap.js'
import * as utils from '../lib/utils.js'
import subsystem from '../lib/rules/subsystem.js'

const { values: parsed, positionals: args } = parseArgs({
  options: {
    help: {
      type: 'boolean',
      short: 'h'
    },
    version: {
      type: 'boolean',
      short: 'v'
    },
    'validate-metadata': {
      type: 'boolean',
      short: 'V'
    },
    tap: {
      type: 'boolean',
      short: 't'
    },
    out: {
      type: 'string',
      short: 'o'
    },
    list: {
      type: 'boolean',
      short: 'l'
    },
    'list-subsystems': {
      type: 'boolean'
    }
  },
  allowNegative: true,
  allowPositionals: true
})

if (parsed.help) {
  const usagePath = path.join(new URL(import.meta.url).pathname, '../usage.txt')
  const help = await fs.promises.readFile(usagePath, 'utf8')
  console.log(help)
  process.exit(0)
}

if (parsed.version) {
  const pkgJsonPath = path.join(new URL(import.meta.url).pathname, '../../package.json')
  const pkgJson = await fs.promises.readFile(pkgJsonPath, 'utf8')
  const { version } = JSON.parse(pkgJson)
  console.log(`core-validate-commit v${version}`)
  process.exit(0)
}

if (!parsed.help && !args.length) { args.push('HEAD') }

function load (sha, cb) {
  // Handle pre-parsed commit objects from stdin
  if (typeof sha === 'object') {
    return process.nextTick(() => {
      cb(null, sha)
    })
  }

  const parsed = URL.parse(sha)
  if (parsed != null) {
    return loadPatch(parsed, cb)
  }
  exec(`git show --quiet --format=medium ${sha}`, (err, stdout, stderr) => {
    if (err) return cb(err)
    cb(null, stdout.trim())
  })
}

function loadPatch (uri, cb) {
  let h = http
  if (~uri.protocol.indexOf('https')) {
    h = https
  }
  const headers = {
    'user-agent': 'core-validate-commit'
  }
  h.get(uri, { headers }, (res) => {
    let buf = ''
    res.on('data', (chunk) => {
      buf += chunk
    })

    res.on('end', () => {
      try {
        const out = JSON.parse(buf)
        cb(null, out)
      } catch (err) {
        cb(err)
      }
    })
  }).on('error', cb)
}

const v = new Validator(parsed)

if (parsed['list-subsystems']) {
  utils.describeSubsystem(subsystem.defaults.subsystems.sort())
  process.exit(0)
}

if (parsed.list) {
  const ruleNames = Array.from(v.rules.keys())
  const max = ruleNames.reduce((m, item) => {
    if (item.length > m) m = item.length
    return m
  }, 0)

  for (const rule of v.rules.values()) {
    utils.describeRule(rule, max)
  }
  process.exit(0)
}

// Don't start processing if reading from stdin (handled in stdin.on('end'))
if (args.length === 1 && args[0] === '-') {
  const chunks = []
  process.stdin.on('data', (chunk) => chunks.push(chunk))
  process.stdin.on('end', () => {
    try {
      const input = Buffer.concat(chunks).toString('utf8')
      const commits = JSON.parse(input)

      if (!Array.isArray(commits)) {
        throw new Error('Input must be an array')
      }

      // Replace args with the commit data directly
      for (let i = 0; i < commits.length; i++) {
        const commit = commits[i]
        if (!commit.id || !commit.message) {
          throw new Error('Each commit must have "id" and "message" properties')
        }
        args[i] = { sha: commit.id, ...commit }
      }
      run()
    } catch (err) {
      console.error('Error parsing JSON input:', err.message)
      process.exit(1)
    }
  })
  process.stdin.resume()
} else {
  run()
}

function run () {
  if (!parsed.tap) {
    v.on('commit', (c) => {
      pretty(c.commit, c.messages, v)
      commitRun()
    })

    commitRun()
    return
  }
  const tap = new Tap()
  tap.pipe(process.stdout)
  if (parsed.out) tap.pipe(fs.createWriteStream(parsed.out))
  let count = 0
  const total = args.length

  v.on('commit', (c) => {
    count++
    const test = tap.test(c.commit.sha)
    formatTap(test, c.commit, c.messages, v)
    if (count === total) {
      setImmediate(() => {
        tap.end()
        if (tap.status === 'fail') { process.exitCode = 1 }
      })
    }
  })

  tapRun()
}

function tapRun () {
  if (!args.length) return
  const sha = args.shift()
  load(sha, (err, data) => {
    if (err) throw err
    v.lint(data)
    tapRun()
  })
}

function commitRun () {
  if (!args.length) {
    process.exitCode = v.errors
    return
  }
  const sha = args.shift()
  load(sha, (err, data) => {
    if (err) throw err
    v.lint(data)
  })
}