Pre Commit

1,472 阅读2分钟

最终效果:每次提交代码时,自动完成以下两个功能

  1. 执行代码增量检查,有代码风格错误抛出异常。

  2. 检查提交的文件列表,如果同时存在 dist 目录和非 dist 目录,抛出异常。

前期准备

在阅读本文之前,请确保熟悉或了解 eslinteslint-stagedhusky.

如果不了解,请点击传送门:Javascript代码检查那点事

代码增量检查

eslint 的具体配置不再赘述,在上述的传送门中有详细配置。

安装相关依赖

npm install --save-dev husky
npm install --save-dev lint-staged

package.json 中增加如下配置

"husky": {
  "hooks": {
    "pre-commit": "node scripts/utils/pre-commit.js"
  }
},
"lint-staged": {
  "linters": {
    "*.js": [
      "eslint",
      "git add"
    ],
    "*.vue": [
      "eslint",
      "git add"
    ]
  },
  "ignore": ["dist/**"]
},

配置说明:当执行 git commit 时,会执行 husky.hooks.pre-commit, 也就是 node scripts/utils/pre-commit.js,该命令为执行一段脚本,脚本如下。

提交文件检查

检查提交内容的核心思路为:获取本次提交的改动文件列表信息 -> 检查文件路径 -> 判断是否同时存在 dist 和非 dist目录,如果同时存在,则抛出异常停止 commit 操作,如果不存在,则执行 eslint 代码检查。详细代码如下(可直接拷贝到 scripts/utils/pre-commit.js 中)

const chalk = require('chalk')
const symbols = require('log-symbols')
const spawn = require('child_process').spawn
require('lint-staged')

// 检查改动的文件目录
getDiffFiles().then(files => {
  const filePaths = files.map(file => file.filename.split('/')[0])
  let isDistFolder = false
  let isOtherFiles = false
  filePaths.forEach(path => {
    if (path === 'dist') isDistFolder = true
    else isOtherFiles = true
  })
  if (isDistFolder && isOtherFiles) {
    throw new Error()
  }
  runCmd('lint-staged')
}).catch(() => {
  /* eslint-disable */
  console.error(`\n\n${symbols.error} ${chalk.redBright('Ops!Dist folder and other files cannot be submitted at the same time.')}`)  
  console.log(`    (use "git reset" to cancel the "add" operation)\n`)
  /* eslint-enable */
  process.exit(1)
})


// 获取本次改动的文件列表
function getDiffFiles() {
  return getHeadCommitId().then(head => {
    if (head) {
      const command = 'git -c core.quotepath=false diff-index --cached --name-status -M --diff-filter=ACM ' + head
      return runCmd(command).then(({ err, stdout, stderr }) => {
        return err || stderr ? err || new Error(stderr) : stdoutToResultsObject(stdout)
      })
    }
  })
}

// 获取最近一次提交的commit_id
function getHeadCommitId() {
  return runCmd('git rev-parse --verify HEAD').then(({ err, stdout, stderr }) => {
    if (err && err.message.indexOf('fatal: Needed a single revision') !== -1) {
      return getFirstCommitId()
    } else {
      return err || stderr ? err || new Error('STDERR: ' + stderr) : stdout.replace('\n', '')
    }
  })
}

// 获取第一次提交的commit_id
function getFirstCommitId() {
  return runCmd('git hash-object -t tree /dev/null').then(({ err, stdout, stderr }) => {
    return err || stderr ? err || new Error('STDERR: ' + stderr) : stdout.replace('\n', '')
  })
}


// 执行命令,监听控制台信息
function runCmd(command) {
  return new Promise((resolve) => {
    // 解析命令获取参数
    const bits = command.split(' ')
    const args = bits.slice(1)

    // 执行命令
    const cmd = spawn(bits[0], args, { cwd: process.cwd() })

    let stdout = ''
    let stderr = ''

    cmd.stdout.on('data', data => {
      stdout += data.toString()
    })

    cmd.stderr.on('data', data => {
      stderr += data.toString()
    })

    cmd.on('close', code => {
      const err = code !== 0 ? new Error(stderr) : null
      resolve({ err, stdout, stderr })
    })
  })
}

// 把stdout输出信息转化成Object对象
function stdoutToResultsObject(stdout) {
  const results = []
  const lines = stdout.split('\n')
  let iLines = lines.length
  while (iLines--) {
    const line = lines[iLines]
    if (line !== '') {
      const parts = line.split('\t')
      const result = {
        filename: parts[2] || parts[1],
        status: codeToStatus(parts[0]),
      }
      results.push(result)
    }
  }
  return results
}

// git 枚举映射
function codeToStatus(code) {
  const map = {
    A: 'Added',
    C: 'Copied',
    D: 'Deleted',
    M: 'Modified',
    R: 'Renamed',
    T: 'Type-Change',
    U: 'Unmerged',
    X: 'Unknown',
    B: 'Broken',
  }
  return map[code.charAt(0)]
}

当执行 git commit 操作时,首先会执行上述脚本,当满足要求时,再执行 eslint 代码检查,当代码检查通过后,才会提交代码到远程仓库!