Skip to content

Commit

Permalink
New method "changelog.openEditor()"
Browse files Browse the repository at this point in the history
This method opens the THOUGHTFUL_CHANGELOG_EDITOR or the EDITOR on the CHANGELOG.md-file
  • Loading branch information
Nils Knappmeier committed Feb 5, 2016
1 parent 5345646 commit 04419c9
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 39 deletions.
3 changes: 2 additions & 1 deletion bin/thoughtful.js
Expand Up @@ -23,7 +23,8 @@ program
console.log(options)
console.log('Updating changelog')
thoughtful.updateChangelog({
release: options.release
release: options.release,
addToGit: options.addToGit
}).done(console.log)
})

Expand Down
15 changes: 14 additions & 1 deletion lib/changelog.js
Expand Up @@ -8,6 +8,7 @@

var qfs = require('q-io/fs')
var path = require('path')
var qcp = require('../lib/q-child-process.js')

module.exports = function (cwd) {
return new Changelog(cwd)
Expand Down Expand Up @@ -71,9 +72,21 @@ function Changelog (cwd) {
/**
* Store the modified file into the changelog
* @returns {Promise.<?>} a promise that is resolved when the file is written
*/
*/
this.save = function () {
return this.contents.then((contents) => qfs.write(this.file, contents)
)
}

/**
* Open an editor on CHANGELOG.md. The command is either taken from the environment variable
* THOUGHTFUL_CHANGELOG_EDITOR or EDITOR. If none is defined, `vi` is used.
* @returns {Promise.<*>} a Promise that is resolved when the editor closes with exit-code zero and rejected
* for any other exit-code.
*/
this.openEditor = function () {
var editor = process.env['THOUGHTFUL_CHANGELOG_EDITOR'] || process.env['EDITOR'] || 'vi'
var file = path.relative(cwd, this.file)
return this.save().then(() => qcp.spawnWithShell(`${editor} ${qcp.escapeParam(file)}`, {env: process.env, cwd: cwd, stdio: 'inherit'}))
}
}
128 changes: 91 additions & 37 deletions lib/q-child-process.js
@@ -1,49 +1,103 @@
var cp = require('child_process')
var Q = require('q')
var debug = require('debug')('thoughtful:q-child-process')

/**
* Promise-Based functions from child_process
*/
module.exports = {
/**
* Execute a child-process and return a promise for the output
* @param cmd
* @param args
* @param options
* @returns {{ stdout: string, stderr: string}} the output of the process on stdout and stderr
*/
execFile: function (cmd, args, options) {
var defer = Q.defer()
cp.execFile(cmd, args, options, (err, stdout, stderr) => {
if (err) {
return defer.reject(err)
}
return defer.resolve({
stdout: stdout,
stderr: stderr,
/**
* Appends first stdout, then stderr to the `output`-string and returns the result
* @param {string} output the previous output
*/
appendTo: function (output) {
var myOutput = `${stdout.trim()}\n${stderr.trim()}`
return `${output}\n${myOutput.trim()}`.trim()
}
})
})
return defer.promise
},
execFile: execFile,
spawn: spawn,
spawnWithShell: spawnWithShell,
escapeParam: escapeParam
}

spawn: function (cmd, args, options) {
var defer = Q.defer()
var child = cp.spawn(cmd, args, options)
child.on('exit', function (code) {
if (code === 0) {
defer.resolve()
} else {
defer.reject()
/**
* Execute a child-process and return a promise for the output
* @param cmd
* @param args
* @param options
* @returns {{ stdout: string, stderr: string}} the output of the process on stdout and stderr
*/
function execFile (cmd, args, options) {
var defer = Q.defer()
cp.execFile(cmd, args, options, (err, stdout, stderr) => {
if (err) {
return defer.reject(err)
}
return defer.resolve({
stdout: stdout,
stderr: stderr,
/**
* Appends first stdout, then stderr to the `output`-string and returns the result
* @param {string} output the previous output
*/
appendTo: function (output) {
var myOutput = `${stdout.trim()}\n${stderr.trim()}`
return `${output}\n${myOutput.trim()}`.trim()
}
})
return defer.promise
})
return defer.promise
}

/**
* Spawn a process and return a promise that is resolved when the process exits with exit-code zero, or rejected
* on a non-zero exit-code.
* @param {string} cmd the command to execute
* @param {string[]} args command line arguments for the command
* @param {object} options options for the {@link child_process#spawn}-method.
* @returns {*} a promise that is resolved or rejected based on the exit-code of the child-process
*/
function spawn (cmd, args, options) {
debug('spawn', cmd, args, options)
var defer = Q.defer()
var child = cp.spawn(cmd, args, options)
child.on('exit', function (code) {
if (code === 0) {
defer.resolve()
debug('spawn resolved', cmd, args, options)
} else {
defer.reject(new Error(`Command "${cmd} ${args && args.join(' ')}" exited with ${code}`))
}
})
return defer.promise
}

/**
* Spawn a process and return a promise that is resolved when the process exits with exit-code zero, or rejected
* on a non-zero exit-code.
* The function uses a shell to spawn the process. The command may be a complete shell command including parameters,
* and pipes in a single string. It is the counter-part to
* [child_process#exec](https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback)
* but without callback and with the ability to inherit the I/O from the parent process.
* @param {string} command a command in a single string
* @param {object=} options options for the {@link child_process#spawn}-method.
*/
function spawnWithShell (command, options) {
var os = require('os')
switch (os.type()) {
case 'Linux':
case 'Dawin':
return spawn('/bin/sh', ['-c', command], options)
case 'Windows_NT':
return spawn('cmd.exe', ['/s', '/c', command], options)
default:
throw new Error(`Unexpected OS-type ${os.type()}`)
}
}

function escapeParam (param) {
var os = require('os')
switch (os.type()) {
case 'Linux':
case 'Dawin':
// Escape " -> \" and \ -> \\
return `"${param.replace(/[\\"]/g, '\\$&')}"`
case 'Windows_NT':
// No escaping in windows until somebody files an issue to do otherwise
return param
default:
throw new Error(`Unexpected OS-type ${os.type()}`)
}
}
50 changes: 50 additions & 0 deletions test/changelog-spec.js
Expand Up @@ -10,6 +10,7 @@
// /* global xdescribe */
// /* global xit */
/* global beforeEach */
/* global afterEach */

'use strict'

Expand Down Expand Up @@ -98,4 +99,53 @@ describe('changelog-library:', () => {
return expect(changelogContents).to.eventually.equal(fixture('changelog-with-two-releases.md'))
})
})

describe('The openEditor-method', function () {
/**
* Return the path to a file within the working dir
*
*/
function workDir () {
const argsAsArray = Array.prototype.slice.apply(arguments)
return path.join.apply(path, ['tmp', 'test', 'changelog-editor'].concat(argsAsArray))
}

// Clear the working directory before each test
beforeEach(() => {
return qfs.removeTree(workDir())
.catch(ignoreENOENT)
.then(() => qfs.makeTree(workDir()))
})

afterEach(() => {
process.env['THOUGHTFUL_CHANGELOG_EDITOR'] = ''
process.env['EDITOR'] = ''
})

it('should call THOUGHTFUL_CHANGELOG_EDITOR as editor, if set', () => {
const dummyEditor = path.resolve(__dirname, 'dummy-editor', 'dummy-editor.js')
process.env['THOUGHTFUL_CHANGELOG_EDITOR'] = `${dummyEditor} changelog-editor`
process.env['EDITOR'] = `${dummyEditor} default-editor`
var changelogContents = changelog(workDir()).openEditor()
.then(() => qfs.read(workDir('CHANGELOG.md')))
return expect(changelogContents).to.eventually.equal(`changelog-editor
# Release-Notes
<a name="current-release"></a>
`)
})

it('should call EDITOR as editor, if THOUGHTFUL_CHANGELOG_EDITOR is not set', () => {
const dummyEditor = path.resolve(__dirname, 'dummy-editor', 'dummy-editor.js')
process.env['THOUGHTFUL_CHANGELOG_EDITOR'] = ''
process.env['EDITOR'] = `${dummyEditor} default-editor`
var changelogContents = changelog(workDir()).openEditor()
.then(() => qfs.read(workDir('CHANGELOG.md')))
return expect(changelogContents).to.eventually.equal(`default-editor
# Release-Notes
<a name="current-release"></a>
`)
})
})
})
14 changes: 14 additions & 0 deletions test/dummy-editor/dummy-editor.js
@@ -0,0 +1,14 @@
#!/usr/bin/env node

// Changelog-Editor for unit-tests
// Usage:
// dummy-editor.js "some text" "file-to-open"
//
// "some text": This text will be inserted at the beginning of the file
// "file-to-open": The file the should be edited
//
var fs = require('fs')
console.log(process.argv)
var contents = fs.readFileSync(process.argv[3], {encoding: 'utf-8'})
contents = contents.replace(/\n+/g, '\n')
fs.writeFileSync(process.argv[3], `${process.argv[2]}\n\n${contents.trim()}\n`)
35 changes: 35 additions & 0 deletions test/q-child-process-spec.js
Expand Up @@ -27,4 +27,39 @@ describe('q-child-process-library:', () => {
.to.be.rejected
})
})

describe('the spawn-shell', () => {
it('should reject the promise if the child-process exits with exit-code!=0', () => {
const cmd = path.resolve(__dirname, 'dummy-git', 'git-add.js')
return expect(qcp.spawnWithShell(cmd + ' add file4.js'))
.to.be.rejected
})
})

describe('the spawn-shell', () => {
it('should fulfull the promise if the child-process exits with exit-code===0', () => {
const cmd = path.resolve(__dirname, 'dummy-git', 'git-add.js')
return expect(qcp.spawnWithShell(cmd + ' add file1.js'))
.not.to.be.rejected
})
})

describe('The escapeParam-method', () => {
var os = require('os')
switch (os.type()) {
case 'Linux':
case 'Dawin':
it('should escape \\ and " on Linux', () => {
expect()
})
break
case 'Windows_NT':
it('should have test-cases for Windows (but does not yet)', () => {
throw new Error('should have test-cases for Windows (but does not yet)')
})
break
default:
throw new Error(`Unexpected OS-type ${os.type()}`)
}
})
})

0 comments on commit 04419c9

Please sign in to comment.