Googleのバグ予測アルゴリズムをnode.jsで実装してみた件

Table of Content

はじめに

Googleのバグ予測アルゴリズムというものがあります。
https://www.publickey1.jp/blog/11/post_193.html

簡単にいうと最近、数多くバグ修正したコードはバグが発生しやすいという考えになっています。

今回はnode.jsでそれを実装してみました。

事前準備

simple-gitをインストールします。

npm install --save simple-git

コード

以下のロジックではコミットログのメッセージにfixという文字が会ったらバグ修正をしたとみなしてます。

const simpleGit = require('simple-git')
const fs = require('fs')

function moveFile(path) {
  moves = path.match(/(?<=\{)[\s\S]*?(?=\})/g)
  if (!moves) return null
  let before = path
  let after = path
  moves.map((move)=>{
    const names = move.split(' => ')
    before = before.replace(move, names[0])
    after = after.replace(move, names[1])
  })
  return {
    before: before.replace(/{|}/g, '').replace(/\/\//g, '/'),
    after: after.replace(/{|}/g, '').replace(/\/\//g, '/')
  }
}
function checkMessage(message) {
  // TODO プロジェクトのルールに合わせてコミットログのメッセージを調整する
  if (message.indexOf('fix') >=0) return true
  return false
}

async function getCommitLog(repUrl, branch, workFolder) {
  const result = {}
  try {
    if (fs.existsSync(workFolder)) {
      fs.rmdirSync(workFolder, { recursive: true })
    }
    fs.mkdirSync(workFolder)
    await simpleGit().clone(repUrl, workFolder,  ['-b', branch])  
    const git = simpleGit(workFolder)
    await git.log({'--stat': null, '--stat-width': 1024}, (err, logs) => {
      //console.log(err, logs)
      logs.all.reverse().map((log)=>{
        //console.log(log.hash, log.date, log.message, log.author_name)
        if (!log.diff) return

        // コミットログのチェックして集計対象か調べる
        const fixedLog = checkMessage(log.message)

        log.diff.files.map((file)=>{
          //console.log('  ', file)
          let fileName = file.file
          if (fileName.indexOf('=>') > 0) {
            const moveResult = moveFile(fileName)
            if (moveResult) {
              fileName = moveResult.after
              if (result[moveResult.before]) {
                result[moveResult.after] = result[moveResult.before]
                delete result[moveResult.before]
              }
            } else {
              console.warn('unexpected filepath' , log.hash, fileName)
              return
            }
          }
          if (!result[fileName]) {
            result[fileName] = {
              file: fileName,
              totalCount: 0,
              createdDate: log.date,
              fixCount: 0,
              fixHistory: []
            }
          }
          ++result[fileName].totalCount
          if (result[fileName].createdDate > log.date) {
            result[fileName].createdDate = log.date
          }
          if (fixedLog) {
            ++result[fileName].fixCount
            result[fileName].fixHistory.push(log.date)
          }

        })
      })
    })
    return result
  }
  catch (e) { 
    console.log(e)
  }
}

function collect(all) {
  const files = []
  const result = []
  for (const key in all) {
    if (all[key].fixCount > 0)  {
      files.push(all[key])
    }
  }
  const baseDate = new Date()
  files.map((file)=>{
    //console.log(file.file)
    const createdDate = new Date(file.createdDate)
    const timeCreateToNow = baseDate-createdDate
    let score = 0
    file.fixHistory.map((history)=>{
      const t = ((new Date(history))-createdDate) / timeCreateToNow
      //console.log(file.createdDate, history, t)
      score = score + (1/(1+Math.exp(-12*t+12)))
    })
    result.push([file.file, score])
  })
  result.sort((a,b)=>b[1]-a[1])
  return result
}

async function main() {
  const all = await getCommitLog('https://github.com/vuejs/vue.git', 'master', './work')
  const result = collect(all)
  console.log(result)
}

main()