Skip to content

Commit

Permalink
Use shelljs for string commands, execa for arguments array
Browse files Browse the repository at this point in the history
  • Loading branch information
webpro committed Mar 2, 2020
1 parent f28878b commit c74a798
Show file tree
Hide file tree
Showing 8 changed files with 60 additions and 39 deletions.
2 changes: 1 addition & 1 deletion config/release-it.json
@@ -1,7 +1,7 @@
{
"hooks": {},
"git": {
"changelog": "git log --pretty=format:*\\ %s\\ (%h) ${latestTag}...HEAD",
"changelog": "git log --pretty=format:\"* %s (%h)\" ${latestTag}...HEAD",
"requireCleanWorkingDir": true,
"requireBranch": false,
"requireUpstream": true,
Expand Down
7 changes: 1 addition & 6 deletions lib/plugin/GitBase.js
Expand Up @@ -4,8 +4,7 @@ const { GitRemoteUrlError, GitNetworkError } = require('../errors');
const Plugin = require('./Plugin');

const options = { write: false };
const changelogFallback = 'git log --pretty format:*\\ %s\\ (%h)';
const changelogOldDefault = 'git log --pretty=format:"* %s (%h)" ${latestTag}...HEAD';
const changelogFallback = 'git log --pretty=format:"* %s (%h)"';

class GitBase extends Plugin {
async init() {
Expand All @@ -27,10 +26,6 @@ class GitBase extends Plugin {
if (!latestTag && changelog.includes('${latestTag}')) {
changelog = changelogFallback;
}
if (changelog === changelogOldDefault) {
// TODO: Fix this properly
changelog = this.config.defaultConfig.git.changelog;
}
return await this.exec(changelog, { context, options });
}

Expand Down
4 changes: 3 additions & 1 deletion lib/plugin/git/Git.js
Expand Up @@ -70,7 +70,9 @@ class Git extends GitBase {

async hasUpstreamBranch() {
const ref = await this.exec('git symbolic-ref HEAD', { options });
const branch = await this.exec(`git for-each-ref --format %(upstream:short) ${ref}`, { options }).catch(() => null);
const branch = await this.exec(`git for-each-ref --format="%(upstream:short)" ${ref}`, { options }).catch(
() => null
);
return Boolean(branch);
}

Expand Down
59 changes: 42 additions & 17 deletions lib/shell.js
@@ -1,6 +1,9 @@
const sh = require('shelljs');
const execa = require('execa');
const debug = require('debug')('release-it:shell');
const { format, clean } = require('./util');
const { format } = require('./util');

sh.config.silent = !debug.enabled;

const noop = Promise.resolve();

Expand All @@ -11,8 +14,14 @@ class Shell {
this.config = container.config;
}

exec(command, options = {}, context = {}) {
if (!command || !command.length) return;
return typeof command === 'string'
? this.execFormattedCommand(format(command, context), options)
: this.execFormattedCommand(command, options);
}

async execFormattedCommand(command, options = {}) {
const [program, ...programArgs] = typeof command === 'string' ? [] : clean(command);
const isDryRun = this.global.isDryRun;
const isWrite = options.write !== false;
const isExternal = options.external === true;
Expand All @@ -24,32 +33,48 @@ class Shell {

this.log.exec(command, { isExternal });

try {
const { stdout: out, stderr } =
typeof command === 'string' ? await execa.command(command) : await execa(program, programArgs);

const stdout = out === '""' ? '' : out;
if (typeof command === 'string') {
return this.execStringCommand(command, options, { isExternal });
} else {
return this.execWithArguments(command, options, { isExternal });
}
}

this.log.verbose(stdout, { isExternal });
execStringCommand(command, options, { isExternal }) {
return new Promise((resolve, reject) => {
sh.exec(command, { async: true }, (code, stdout, stderr) => {
stdout = stdout.toString().trim();
this.log.verbose(stdout, { isExternal });
debug({ command, options, code, stdout, stderr });
if (code === 0) {
resolve(stdout);
} else {
if (stdout && stderr) {
this.log.log(`\n${stdout}`);
}
reject(new Error(stderr || stdout));
}
});
});
}

debug({ command, stdout, stderr });
async execWithArguments(command, options, { isExternal }) {
const [program, ...programArgs] = command;

try {
const { stdout: out, stderr } = await execa(program, programArgs);
const stdout = out === '""' ? '' : out;
this.log.verbose(stdout, { isExternal });
debug({ command, options, stdout, stderr });
return Promise.resolve(stdout || stderr);
} catch (error) {
if (error.stdout) {
this.log.log(error.stdout);
this.log.log(`\n${error.stdout}`);
}
debug({ error });
return Promise.reject(new Error(error.stderr || error.message));
}
}

exec(command, options = {}, context = {}) {
if (!command) return;
return typeof command === 'string'
? this.execFormattedCommand(format(command, context), options)
: this.execFormattedCommand(command, options);
}
}

module.exports = Shell;
12 changes: 0 additions & 12 deletions test/git.init.js
Expand Up @@ -104,15 +104,3 @@ test.serial('should generate correct changelog', async t => {
const changelog = gitClient.getContext('changelog');
t.regex(changelog, /\* Add file \(\w{7}\)\n\* Add file \(\w{7}\)/);
});

test.serial('should generate correct changelog (backwards compat)', async t => {
const gitOptions = Object.assign({}, git, { changelog: 'git log --pretty=format:"* %s (%h)" ${latestTag}...HEAD' });
const options = { git: gitOptions };
const gitClient = factory(Git, { options });
sh.exec('git tag 1.0.0');
gitAdd('line', 'file', 'Add file');
gitAdd('line', 'file', 'Add file');
await gitClient.init();
const changelog = gitClient.getContext('changelog');
t.regex(changelog, /\* Add file \(\w{7}\)\n\* Add file \(\w{7}\)/);
});
2 changes: 1 addition & 1 deletion test/git.js
Expand Up @@ -233,7 +233,7 @@ test.serial('should return repo status', async t => {
sh.ShellString('line').toEnd('file1');
sh.ShellString('line').toEnd('file2');
sh.exec('git add file2');
t.is(await gitClient.status(), ' M file1\nA file2');
t.is(await gitClient.status(), 'M file1\nA file2');
});

test.serial('should reset files', async t => {
Expand Down
6 changes: 5 additions & 1 deletion test/npm.js
Expand Up @@ -46,8 +46,10 @@ test('should derive tag from pre-release version', async t => {
test('should use provided (default) tag even for pre-release', async t => {
const options = { npm: { tag: 'latest' } };
const npmClient = factory(npm, { options });
const exec = sinon.stub(npmClient.shell, 'exec').resolves();
await npmClient.bump('1.0.0-next.0');
t.is(npmClient.getContext('tag'), 'latest');
exec.restore();
});

test('should warn when bumping to same version', async t => {
Expand Down Expand Up @@ -109,7 +111,9 @@ test('should add registry to commands when specified', async t => {

test('should not throw when executing tasks', async t => {
const npmClient = factory(npm);
const exec = sinon.stub(npmClient.shell, 'exec').resolves();
await t.notThrowsAsync(runTasks(npmClient));
exec.restore();
});

test('should throw if npm is down', async t => {
Expand Down Expand Up @@ -211,7 +215,7 @@ test('should handle 2FA and publish with OTP', async t => {

test('should publish', async t => {
const npmClient = factory(npm);
const exec = sinon.spy(npmClient.shell, 'exec');
const exec = sinon.stub(npmClient.shell, 'exec').resolves();
await runTasks(npmClient);
t.is(exec.lastCall.args[0].trim(), 'npm publish . --tag latest');
exec.restore();
Expand Down
7 changes: 7 additions & 0 deletions test/shell.js
Expand Up @@ -18,6 +18,13 @@ test('exec (with context)', async t => {
t.is(await exec('echo -*- ${github.tokenRef} -*-'), '-*- GITHUB_TOKEN -*-');
});

test('exec (with args)', async t => {
t.is(await shell.exec([]), undefined);
t.is(await shell.exec(['pwd']), cwd);
t.is(await shell.exec(['echo', 'a', 'b']), 'a b');
t.is(await shell.exec(['echo', '"a"']), '"a"');
});

test('exec (dry-run/read-only)', async t => {
const shell = factory(Shell, { global: { isDryRun: true } });
{
Expand Down

0 comments on commit c74a798

Please sign in to comment.