komit
Version:
An interactive prompt for conventional commit messages that doesn't get in your way. Designed to run as a git hook.
171 lines (151 loc) • 3.84 kB
JavaScript
const fs = require('fs')
const cp = require('child_process')
const path = require('path')
const { forceStdinTty } = require('force-stdin-tty')
const { prompt } = require('enquirer')
const c = require('ansi-colors')
const pkg = require('./package.json')
const msgPath = path.resolve(process.cwd(), process.argv[process.argv.length - 1])
const scopesRc = path.resolve(process.cwd(), '.commitscopesrc')
const scopes = []
let type, scope, headerPrefix, maxTitleLength
process.stdout.write(c.bold('\n👮 Welcome to Komit\n') + c.dim(`v${pkg.version}\n\n`))
let overwroteStdin = false
try {
overwroteStdin = forceStdinTty()
} catch {
process.stdout.write(c.bold.red("⚠️ Komit couldn't access your stdin stream.\n"))
process.exit(1)
}
try {
fs.readFileSync(scopesRc, {
encoding: 'utf8'
}).split('\n').forEach(line => {
if (line.startsWith('#') || line.trim().length === 0) return
scopes.push(line.toLowerCase().trim().replace(/ /g, '-'))
})
} catch(error) {
// ignore
}
function scopeSuggest(input, choices) {
let newPrompt = true
const list = choices.filter(ch => {
if (ch.name === 'new' || ch.name === '(unscoped)') return false
const test = ch.message.toLowerCase()
if (test === input.toLowerCase().trim().replace(/ /g, '-')) {
newPrompt = false
}
return test.includes(input.toLowerCase().trim().replace(/ /g, '-'))
})
if (newPrompt && input) {
list.push({
name: 'new',
normalized: true,
message: `🏷️ New scope: ${input.toLowerCase().trim().replace(/ /g, '-')}`,
value: input.toLowerCase().trim().replace(/ /g, '-')
})
}
if (input.length === 0) {
list.unshift({
name: '(unscoped)',
normalized: true,
message: '(unscoped)',
value: '(unscoped)'
})
}
return list
}
prompt([
{
type: 'autocomplete',
name: 'type',
message: 'Commit type:',
choices: [
'build',
'ci',
'chore',
'docs',
'feat',
'fix',
'perf',
'refactor',
'revert',
'style',
'test'
],
result: result => {
type = result
return result
}
},
{
type: 'autocomplete',
name: 'scope',
message: 'Scope:',
choices: [
'(unscoped)',
...scopes
],
suggest: scopeSuggest,
result: result => {
scope = result
headerPrefix = `${type}${(scope !== '(unscoped)') ? `(${scope})` : ''}: `
maxTitleLength = 100 - headerPrefix.length
return result
}
},
{
type: 'input',
name: 'title',
message: 'Title:',
validate: title => {
if (title.length === 0) {
return 'Commit title cannot be empty!'
}
if (title.length > maxTitleLength) {
return `Title is too long (${title.length}/${maxTitleLength}) characters)`
}
if (title[0].charCodeAt(0) !== title[0].toLowerCase().charCodeAt(0)) {
return 'Title must start in lowercase'
}
if (title[title.length - 1] === '.') {
return 'Title cannot end with a full stop "."'
}
return true
}
}
]).then(answer => {
if (answer.scope !== '(unscoped)' && !scopes.includes(answer.scope)) {
scopes.push(answer.scope)
fs.writeFileSync(scopesRc, scopes.join('\n'))
process.stdout.write(c.dim('Updated commit scopes file at .commitscopesrc, adding file to commit.\n'))
cp.execSync(`git add ${scopesRc}`, {
cwd: process.cwd()
})
}
const commitHeader = `${headerPrefix}${answer.title}`
process.stdout.write(
'\n' +
c.bold.green(type) +
(scope !== '(unscoped)' ? `(${c.italic.blue(scope)}): ` : ': ') +
c.bold.underline(answer.title) +
'\n'
)
fs.writeFileSync(msgPath, commitHeader)
if (overwroteStdin) {
process.stdin.destroy()
}
process.exit(0)
}).catch(error => {
if (overwroteStdin) {
process.stdin.destroy()
}
if (error !== '') {
process.stdout.write(c.bold.red(`${error}\n`))
process.exit(1)
} else {
process.stdout.write('\nGot SIGINT, bypassing prompts...\n')
process.exit(0)
}
})