Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions packages/sui-mono/bin/sui-mono-commit.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
/* eslint no-console:0 */
const {promisify} = require('util')
const exec = promisify(require('child_process').exec)
const {Command} = require('commander')

const startMainCommitFlow = require('../src/prompter-manager.js')
const {executeCommit} = startMainCommitFlow

const program = new Command()
program
.option('--no-interactive', 'Skip interactive prompts, requires --type, --scope, --subject')
.option('-t, --type <type>', 'Commit type (feat, fix, docs, refactor, perf, test, chore, release)')
.option('-s, --scope <scope>', 'Commit scope (package name or Root)')
.option('-m, --subject <subject>', 'Commit subject (short description)')
.option('-b, --body <body>', 'Commit body (optional, use | for newlines)')
.option('--breaking <breaking>', 'Breaking changes description (optional)')
.option('--footer <footer>', 'Issues closed (optional, e.g. #31, #34)')
.allowUnknownOption()
.parse(process.argv)

const opts = program.opts()

/**
* Get the list of modified files by the user
Expand Down Expand Up @@ -35,6 +51,17 @@ async function initCommit() {
return
}

if (opts.interactive === false) {
const {type, scope, subject, body, breaking, footer} = opts
if (!type || !scope || !subject) {
console.error('Error: --type, --scope, and --subject are required in non-interactive mode')
process.exit(1)
}
const extraArgs = program.args.join(' ')
await executeCommit({type, scope, subject, body, breaking, footer}, extraArgs)
return
}

startMainCommitFlow()
}

Expand Down
23 changes: 14 additions & 9 deletions packages/sui-mono/src/prompter-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,17 @@ const checkIfHasChangedFiles = async path => {
return stdout.trim() !== ''
}

async function executeCommit(answers, extraArgs = '') {
const commitMsg = buildCommit(answers)
const commitParams = commitMsg
.split('\n')
.filter(Boolean)
.map(msg => `-m "${msg}"`)
.join(' ')

await exec(`git commit ${commitParams} ${extraArgs}`, {cwd: process.cwd()})
}

module.exports = async function startMainCommitFlow() {
const scopesWithChanges = await Promise.all(
scopes.map(pkg => checkIfHasChangedFiles(pkg.name).then(hasFiles => hasFiles && pkg))
Expand All @@ -93,17 +104,11 @@ module.exports = async function startMainCommitFlow() {
})

if (answers && answers.confirmCommit === true) {
const commitMsg = buildCommit(answers)
const commitParams = commitMsg
.split('\n') // separate each new line to
.filter(Boolean) // filter empty strings
.map(msg => `-m "${msg}"`)
.join(' ')

const commitArgs = process.argv.slice(2).join(' ')

await exec(`git commit ${commitParams} ${commitArgs}`, {cwd: process.cwd()})
await executeCommit(answers, commitArgs)
} else {
console.log(colors.red('Commit has been canceled.'))
}
}

module.exports.executeCommit = executeCommit
106 changes: 106 additions & 0 deletions packages/sui-mono/test/server/commitSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {expect} from 'chai'

import buildCommit from '../../src/build-commit.js'

describe('build-commit', () => {
it('builds a basic commit message', () => {
const msg = buildCommit({type: 'feat', scope: 'Root', subject: 'add feature'})
expect(msg).to.equal('feat(Root): add feature')
})

it('lowercases the first letter of subject', () => {
// buildCommit itself does not lowercase — that is done by the enquirer filter.
// In non-interactive mode the subject is passed as-is, so this test confirms
// the raw output.
const msg = buildCommit({type: 'fix', scope: 'Root', subject: 'Fix bug'})
expect(msg).to.equal('fix(Root): Fix bug')
})

it('appends body when provided', () => {
const msg = buildCommit({type: 'feat', scope: 'Root', subject: 'add feature', body: 'details'})
expect(msg).to.include('\n\ndetails')
})

it('replaces | with newlines in body', () => {
const msg = buildCommit({
type: 'feat',
scope: 'Root',
subject: 'add feature',
body: 'line1|line2'
})
expect(msg).to.include('line1\nline2')
})

it('appends breaking changes section when provided', () => {
const msg = buildCommit({
type: 'feat',
scope: 'Root',
subject: 'new api',
breaking: 'old api removed'
})
expect(msg).to.include('BREAKING CHANGES: \nold api removed')
})

it('appends issues closed section when footer provided', () => {
const msg = buildCommit({type: 'fix', scope: 'Root', subject: 'fix', footer: '#42'})
expect(msg).to.include('ISSUES CLOSED: #42')
})

it('omits breaking changes section when breaking is empty', () => {
const msg = buildCommit({type: 'feat', scope: 'Root', subject: 'add feature', breaking: ''})
expect(msg).to.not.include('BREAKING CHANGES')
})

it('omits issues closed section when footer is empty', () => {
const msg = buildCommit({type: 'feat', scope: 'Root', subject: 'add feature', footer: ''})
expect(msg).to.not.include('ISSUES CLOSED')
})

it('builds the -m params string used by executeCommit', () => {
const msg = buildCommit({
type: 'feat',
scope: 'packages/sui-mono',
subject: 'add non-interactive mode',
body: 'useful for scripting'
})
const commitParams = msg
.split('\n')
.filter(Boolean)
.map(m => `-m "${m}"`)
.join(' ')

expect(commitParams).to.include('-m "feat(packages/sui-mono): add non-interactive mode"')
expect(commitParams).to.include('-m "useful for scripting"')
})
})

describe('non-interactive CLI validation', () => {
// Mirrors the validation logic in bin/sui-mono-commit.js initCommit()
const REQUIRED = ['type', 'scope', 'subject']

const validate = opts => REQUIRED.filter(f => !opts[f])

it('passes when type, scope and subject are all provided', () => {
expect(validate({type: 'feat', scope: 'Root', subject: 'test'})).to.deep.equal([])
})

it('fails when type is missing', () => {
expect(validate({scope: 'Root', subject: 'test'})).to.include('type')
})

it('fails when scope is missing', () => {
expect(validate({type: 'feat', subject: 'test'})).to.include('scope')
})

it('fails when subject is missing', () => {
expect(validate({type: 'feat', scope: 'Root'})).to.include('subject')
})

it('fails with all three when nothing is provided', () => {
expect(validate({})).to.deep.equal(['type', 'scope', 'subject'])
})

it('body, breaking and footer are optional — no validation error', () => {
expect(validate({type: 'chore', scope: 'Root', subject: 'update'})).to.have.length(0)
})
})
Loading