tunnckoCore/parse-git-log

View on GitHub
index.js

Summary

Maintainability
B
5 hrs
Test Coverage
/*!
 * parse-git-log <https://github.com/tunnckoCore/parse-git-log>
 *
 * Copyright (c) Charlike Mike Reagent <@tunnckoCore> (http://i.am.charlike.online)
 * Released under the MIT license.
 */

'use strict'

const path = require('path')
const vfile = require('vfile')
const spawn = require('cross-spawn')
const split2 = require('split2')
const through2 = require('through2')

/**
 * Helpers
 */

const delimParts = '~&>8~#@~8<&~'
const delimiter = '~!---------------------- >8~ ----------------------!~'
const formats = [
  '%P', '%H', '%at', '%b', '%T',
  '%an', '%ae', '%ar', '%aI',
  '%s', '%D'
].join(delimParts)

/**
 * > Parses an advanced `git log` output using
 * streams. Allows custom `plugin` function to be passed
 * to update/modify the commit object (which is [vfile][]).
 * It also emits `data` and `commit` events, so you may not
 * need such `plugin` function. But in case you want to do some
 * more parsing and interesting stuff, this `plugin` function
 * allows you to do cool things. That `plugin` function is called
 * with "transform stream" context and this is also passed as
 * first argument. When you return a function from that plugin,
 * it is called with [vfile][] object (commit object) as first argument.
 *
 * **Example**
 *
 * ```js
 * const parseGitLog = require('parse-git-log')
 *
 * // optionally pass `cwd` as first argument
 * parseGitLog()
 *   .once('error', (err) => console.error('err:', err))
 *   .on('commit', (commit) => console.log('commit:', commit))
 *   .once('finish', () => console.log('done'))
 * ```
 *
 * @name   parseGitLog
 * @emits `commit` passed with [vfile][] object for each commit
 * @emits `data` same as `commit` event; passed with [vfile][] object for each commit
 * @param  {String} `[cwd]` path to where is the `.git` folder; defaults to `process.cwd()`
 * @param  {Function} `[plugin]` smart plugin function, passed with `stream, file` signature,
 *                               if returns another function, that function is passed
 *                               with `file` object which represent each commit object.
 * @return {Stream} transform stream, using [through2][] behind
 * @api public
 */

const parseGitLog = module.exports = function parseGitLog (cwd, plugin) {
  cwd = typeof cwd === 'string' ? cwd : process.cwd()
  plugin = typeof plugin === 'function' ? plugin : () => () => {}

  const format = '--format=' + formats + delimiter

  let id = 0
  let stream = through2.obj()

  const gitDir = path.resolve(cwd, '.git')
  const proc = spawn('git', ['--git-dir=' + gitDir, 'log', format])

  // transform command output chunks to virtual "files"
  // with useful data on it
  const transform = through2.obj(function (chunk, enc, callback) {
    chunk = chunk.toString().trim()

    // we don't want to process empty chunks
    if (!chunk.length) {
      return callback()
    }

    // create virtual "file" from each chunk (for each commit)
    const file = createFile(delimParts, chunk)
    file.id = id++

    // allow `file.push(file)`, `this.push(file)`
    // and `stream.push(file)`, so from everywhere you want
    file.push = stream.push.bind(stream)

    // call plugin
    const fn = plugin.call(stream, stream, file)
    if (typeof fn === 'function') {
      // call the smart plugin
      fn.call(file, file)
    }

    // emit `data` and `commit` events
    stream.emit('commit', file)
    stream.push(file)
    callback()
  })

  // get errors (in most cases)
  let stderr = ''
  proc.stderr.on('data', function (buf) {
    stderr += buf.toString().trim()
  })

  // done callback on child_proc close/error events
  function done (err, c) {
    // save originals
    let a = err
    let b = c

    // cleanup
    proc.removeListener('close', done)
    proc.removeListener('error', done)

    // close can give us error code
    let code = typeof err === 'number' ? err : err && err.code

    // if first arg is zero we are fine
    if (code === 0) {
      stream.emit('finish')

      // close event is always last,
      // and always fired
      stream.emit('close', a, b)
      return
    }

    // if first arg is error passthrough
    if (typeof err === 'object') {
      stream.emit('error', err)

      // close event is always last,
      // and always fired
      stream.emit('close', a, b)
      return
    }

    // otherwise create error from child_proc's stderr
    err = new Error(stderr)
    // set err code if we have it from close
    err.code = code
    stream.emit('error', err)

    // close event is always last,
    // and always fired
    stream.emit('close', a, b)
  }

  // error from main `proc` is rarely fired
  // I even not sure enough (we have proc.stderr for this?),
  // but we should try to handle it
  proc.once('error', done)

  // useful to get the code and create
  // new error object combined with the string/buffer
  // that comes from `proc.stderr`
  proc.once('close', done)

  // split and transform
  // the command output
  proc.stdout
    .pipe(split2(delimiter))
    .pipe(transform)

  return stream
}

/**
 * > Creates a virtual file ([vfile][]) chunk metadata,
 * which represents the commit object,
 * full with useful metadata for it.
 *
 * @param  {String} `delimParts`
 * @param  {String} `chunk`
 * @return {Object} virtual file, [vfile][]
 */

const createFile = (delimParts, chunk) => {
  let str = chunk
  str = str.charAt(1) === ')' ? str.slice(2) : str
  str = str.charAt(str.length - 2) === '<'
    ? str.slice(0, str.length - 2)
    : str

  const m = str.split(delimParts)
  const parents = m[0].length ? m[0].split(' ') : []
  const body = m[3] && m[3].trim()
  const contents = body && body.length ? m[9] + '\n\n' + body : m[9]

  return vfile({
    path: m[1] + '-' + m[2],
    contents: contents,
    data: {
      // chuck which is coming directly
      // from `git log` command
      chunk: chunk,

      // parsed parts by special delimiter
      chunks: m,

      // from header can get `type`, scope, etc
      // from body can get references, mentions, footer
      header: m[9],
      body: body,

      // general commit info
      ref: m[10],
      tree: m[4],
      hash: m[1],
      abbrev: m[1].slice(0, 7),
      parent: parents,

      // author info
      author: {
        name: m[5],
        email: m[6],
        timestamp: +m[2]
      },

      // dates of commit
      date: {
        relative: m[7],
        unix: +m[2],
        iso: m[8]
      },
      timestamp: +m[2]
    }
  })
}

/**
 * > Thin Promise wrapper over the streaming API.
 *
 * **Example**
 *
 * ```js
 * const parseGitLog = require('parse-git-log')
 *
 * parseGitLog.promise('../foo-bar')
 *   .then((commits) => {
 *     console.log('list of commit objects:')
 *     commits.forEach((commit) => console.log('commit:', commit))
 *   })
 *   .catch(console.error)
 * ```
 *
 * @name   .promise
 * @param  {String} `[cwd]` path to where is the `.git` folder; defaults to `process.cwd()`
 * @param  {Function} `[plugin]` smart plugin function, passed with `stream, file` signature,
 *                               if returns another function, that function is passed
 *                               with `file` object which represent each commit object.
 * @return {Promise} resolves array of [vfile][] commit objects, otherwise rejected promise
 * @api public
 */

parseGitLog.promise = (cwd, plugin) => new Promise((resolve, reject) => {
  const commits = []
  parseGitLog(cwd, plugin)
    .once('error', reject)
    .on('commit', (commit) => commits.push(commit))
    .once('finish', () => {
      resolve(commits)
    })
})