From d15905c0d5acaa0847173cc3588deb40d2848301 Mon Sep 17 00:00:00 2001 From: pvdlg Date: Sun, 29 Apr 2018 00:54:10 -0400 Subject: [PATCH] fix: verify the local branch is up to date with the remote one --- index.js | 11 ++++++++++- lib/git.js | 12 ++++++++++++ test/git.test.js | 33 +++++++++++++++++++++++++++++++ test/helpers/git-utils.js | 12 +++++++++++- test/index.test.js | 41 ++++++++++++++++++++++++++++++++++++++- 5 files changed, 106 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 983a3c7a..2abb32b6 100644 --- a/index.js +++ b/index.js @@ -13,7 +13,7 @@ const getLastRelease = require('./lib/get-last-release'); const {extractErrors} = require('./lib/utils'); const getGitAuthUrl = require('./lib/get-git-auth-url'); const logger = require('./lib/logger'); -const {unshallow, verifyAuth, gitHead: getGitHead, tag, push} = require('./lib/git'); +const {unshallow, verifyAuth, isBranchUpToDate, gitHead: getGitHead, tag, push} = require('./lib/git'); const getError = require('./lib/get-error'); marked.setOptions({renderer: new TerminalRenderer()}); @@ -47,6 +47,15 @@ async function run(options, plugins) { await verify(options); options.repositoryUrl = await getGitAuthUrl(options); + + if (!await isBranchUpToDate(options.branch)) { + logger.log( + "The local branch %s is behind the remote one, therefore a new version won't be published.", + options.branch + ); + return false; + } + if (!await verifyAuth(options.repositoryUrl, options.branch)) { throw getError('EGITNOPERMISSION', {options}); } diff --git a/lib/git.js b/lib/git.js index 160d0aa8..9ca53955 100644 --- a/lib/git.js +++ b/lib/git.js @@ -131,6 +131,17 @@ async function verifyTagName(tagName) { } } +/** + * Verify the local branch is up to date with the remote one. + * + * @param {String} branch The repository branch for which to verify status. + * + * @return {Boolean} `true` is the HEAD of the current local branch is the same as the HEAD of the remote branch, `false` otherwise. + */ +async function isBranchUpToDate(branch) { + return isRefInHistory(await execa.stdout('git', ['rev-parse', `${branch}@{u}`])); +} + module.exports = { gitTagHead, gitTags, @@ -143,4 +154,5 @@ module.exports = { tag, push, verifyTagName, + isBranchUpToDate, }; diff --git a/test/git.test.js b/test/git.test.js index 221a7863..5ca650f5 100644 --- a/test/git.test.js +++ b/test/git.test.js @@ -11,6 +11,7 @@ import { gitTags, isGitRepo, verifyTagName, + isBranchUpToDate, } from '../lib/git'; import { gitRepo, @@ -22,6 +23,8 @@ import { gitAddConfig, gitCommitTag, gitRemoteTagHead, + push as pushUtil, + reset, } from './helpers/git-utils'; // Save the current working diretory @@ -183,3 +186,33 @@ test.serial('Throws error if obtaining the tags fails', async t => { await t.throws(gitTags()); }); + +test.serial('Return "true" if repository is up to date', async t => { + await gitRepo(true); + await gitCommits(['First']); + await pushUtil(); + + t.true(await isBranchUpToDate('master')); +}); + +test.serial('Return falsy if repository is not up to date', async t => { + await gitRepo(true); + await gitCommits(['First']); + await gitCommits(['Second']); + await pushUtil(); + + t.true(await isBranchUpToDate('master')); + + await reset(); + + t.falsy(await isBranchUpToDate('master')); +}); + +test.serial('Return "true" if local repository is ahead', async t => { + await gitRepo(true); + await gitCommits(['First']); + await pushUtil(); + await gitCommits(['Second']); + + t.true(await isBranchUpToDate('master')); +}); diff --git a/test/helpers/git-utils.js b/test/helpers/git-utils.js index 25951ae0..cbd0415b 100644 --- a/test/helpers/git-utils.js +++ b/test/helpers/git-utils.js @@ -98,6 +98,7 @@ export async function gitGetCommits(from) { * Checkout a branch on the current git repository. * * @param {String} branch Branch name. + * @param {Boolean} create `true` to create the branche ans switch, `false` to only switch. */ export async function gitCheckout(branch, create = true) { await execa('git', create ? ['checkout', '-b', branch] : ['checkout', branch]); @@ -208,6 +209,15 @@ export async function gitCommitTag(gitHead) { * @param {String} branch The branch to push. * @throws {Error} if the push failed. */ -export async function push(origin, branch) { +export async function push(origin = 'origin', branch = 'master') { await execa('git', ['push', '--tags', origin, `HEAD:${branch}`]); } + +/** + * Reset repository to a commit. + * + * @param {String} [commit='HEAD~1'] Commit reference to reset the repo to. + */ +export async function reset(commit = 'HEAD~1') { + await execa('git', ['reset', commit]); +} diff --git a/test/index.test.js b/test/index.test.js index a0c51630..644f0999 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -14,6 +14,7 @@ import { gitRemoteTagHead, push, gitShallowClone, + reset, } from './helpers/git-utils'; // Save the current process.env @@ -57,6 +58,7 @@ test.serial('Plugins are called with expected values', async t => { await gitTagVersion('v1.0.0'); // Add new commits to the master branch commits = (await gitCommits(['Second'])).concat(commits); + await push(); const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'}; const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'}; @@ -162,6 +164,7 @@ test.serial('Use custom tag format', async t => { await gitCommits(['First']); await gitTagVersion('test-1.0.0'); await gitCommits(['Second']); + await push(); const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'test-2.0.0'}; const notes = 'Release notes'; @@ -198,6 +201,7 @@ test.serial('Use new gitHead, and recreate release notes if a prepare plugin cre await gitTagVersion('v1.0.0'); // Add new commits to the master branch commits = (await gitCommits(['Second'])).concat(commits); + await push(); const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'}; const notes = 'Release notes'; @@ -257,6 +261,7 @@ test.serial('Call all "success" plugins even if one errors out', async t => { await gitTagVersion('v1.0.0'); // Add new commits to the master branch await gitCommits(['Second']); + await push(); const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'}; const notes = 'Release notes'; @@ -304,6 +309,7 @@ test.serial('Log all "verifyConditions" errors', async t => { const repositoryUrl = await gitRepo(true); // Add commits to the master branch await gitCommits(['First']); + await push(); const error1 = new Error('error 1'); const error2 = new SemanticReleaseError('error 2', 'ERR2'); @@ -346,6 +352,7 @@ test.serial('Log all "verifyRelease" errors', async t => { await gitTagVersion('v1.0.0'); // Add new commits to the master branch await gitCommits(['Second']); + await push(); const error1 = new SemanticReleaseError('error 1', 'ERR1'); const error2 = new SemanticReleaseError('error 2', 'ERR2'); @@ -382,6 +389,7 @@ test.serial('Dry-run skips publish and success', async t => { await gitTagVersion('v1.0.0'); // Add new commits to the master branch await gitCommits(['Second']); + await push(); const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'}; const notes = 'Release notes'; @@ -430,6 +438,7 @@ test.serial('Dry-run skips fail', async t => { await gitTagVersion('v1.0.0'); // Add new commits to the master branch await gitCommits(['Second']); + await push(); const error1 = new SemanticReleaseError('error 1', 'ERR1'); const error2 = new SemanticReleaseError('error 2', 'ERR2'); @@ -464,6 +473,7 @@ test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', a await gitTagVersion('v1.0.0'); // Add new commits to the master branch await gitCommits(['Second']); + await push(); const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'}; const notes = 'Release notes'; @@ -513,6 +523,7 @@ test.serial('Allow local releases with "noCi" option', async t => { await gitTagVersion('v1.0.0'); // Add new commits to the master branch await gitCommits(['Second']); + await push(); const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'}; const notes = 'Release notes'; @@ -566,6 +577,7 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins', await gitTagVersion('v1.0.0'); // Add new commits to the master branch commits = (await gitCommits(['Second'])).concat(commits); + await push(); const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'}; const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'}; @@ -623,6 +635,27 @@ test.serial('Returns falsy value if triggered by a PR', async t => { ); }); +test.serial('Returns falsy value if triggered on an outdated clone', async t => { + // Create a git repository, set the current working directory at the root of the repo + const repositoryUrl = await gitRepo(true); + // Add commits to the master branch + await gitCommits(['First']); + await gitCommits(['Second']); + await push(); + await reset(); + + const semanticRelease = proxyquire('..', { + './lib/logger': t.context.logger, + 'env-ci': () => ({isCi: true, branch: 'master', isPr: false}), + }); + + t.falsy(await semanticRelease({repositoryUrl})); + t.deepEqual(t.context.log.args[t.context.log.args.length - 1], [ + "The local branch %s is behind the remote one, therefore a new version won't be published.", + 'master', + ]); +}); + test.serial('Returns falsy value if not running from the configured branch', async t => { // Create a git repository, set the current working directory at the root of the repo const repositoryUrl = await gitRepo(true); @@ -656,6 +689,7 @@ test.serial('Returns falsy value if there is no relevant changes', async t => { const repositoryUrl = await gitRepo(true); // Add commits to the master branch await gitCommits(['First']); + await push(); const analyzeCommits = stub().resolves(); const verifyRelease = stub().resolves(); @@ -685,7 +719,10 @@ test.serial('Returns falsy value if there is no relevant changes', async t => { t.is(verifyRelease.callCount, 0); t.is(generateNotes.callCount, 0); t.is(publish.callCount, 0); - t.is(t.context.log.args[7][0], 'There are no relevant changes, so no new version is released.'); + t.is( + t.context.log.args[t.context.log.args.length - 1][0], + 'There are no relevant changes, so no new version is released.' + ); }); test.serial('Exclude commits with [skip release] or [release skip] from analysis', async t => { @@ -702,6 +739,7 @@ test.serial('Exclude commits with [skip release] or [release skip] from analysis 'Test commit\n\n commit body\n[skip release]', 'Test commit\n\n commit body\n[release skip]', ]); + await push(); const analyzeCommits = stub().resolves(); const config = {branch: 'master', repositoryUrl, globalOpt: 'global'}; const options = { @@ -826,6 +864,7 @@ test.serial('Throw an Error if plugin returns an unexpected value', async t => { await gitTagVersion('v1.0.0'); // Add new commits to the master branch await gitCommits(['Second']); + await push(); const verifyConditions = stub().resolves(); const analyzeCommits = stub().resolves('string');