Skip to content

Commit

Permalink
perf: use git tag --merge <branch> to filter tags present in a bran…
Browse files Browse the repository at this point in the history
…ch history

BREAKING CHANGE: Git CLI version 2.7.1 or higher is now required

The `--merge` option of the `git tag` command has been added in Git version 2.7.1 and is now used by semantic-release
  • Loading branch information
pvdlg committed Nov 1, 2019
1 parent 844e0b0 commit cffe9a8
Show file tree
Hide file tree
Showing 6 changed files with 23 additions and 25 deletions.
2 changes: 1 addition & 1 deletion bin/semantic-release.js
Expand Up @@ -10,7 +10,7 @@ var execa = require('execa');
var findVersions = require('find-versions');
var pkg = require('../package.json');

var MIN_GIT_VERSION = '2.0.0';
var MIN_GIT_VERSION = '2.7.1';

if (!semver.satisfies(process.version, pkg.engines.node)) {
console.error(
Expand Down
4 changes: 4 additions & 0 deletions docs/support/FAQ.md
Expand Up @@ -238,6 +238,10 @@ In addition the [verify conditions step](../../README.md#release-steps) verifies

See [Node version requirement](./node-version.md#node-version-requirement) for more details and solutions.

## Why does semantic-release require Git version >= 2.7.1?

**semantic-release** uses Git CLI commands to read information about the repository such as branches, commit history and tags. Certain commands and options (such as [the `--merged` option of the `git tag` command](https://git-scm.com/docs/git-tag/2.7.0#git-tag---no-mergedltcommitgt) or bug fixes related to `git ls-files`) used by **semantic-release** are only available in Git version 2.7.1 and higher.

## What is npx?

[`npx`](https://www.npmjs.com/package/npx) – short for "npm exec" – is a CLI to find and execute npm binaries within the local `node_modules` folder or in the $PATH. If a binary can't be located npx will download the required package and execute it from its cache location.
Expand Down
25 changes: 9 additions & 16 deletions lib/branches/get-tags.js
Expand Up @@ -2,33 +2,26 @@ const {template, escapeRegExp} = require('lodash');
const semver = require('semver');
const pReduce = require('p-reduce');
const debug = require('debug')('semantic-release:get-tags');
const {getTags, isRefInHistory, getTagHead} = require('../../lib/git');
const {getTags, getTagHead} = require('../../lib/git');

module.exports = async ({cwd, env, options: {tagFormat}}, branches) => {
// Generate a regex to parse tags formatted with `tagFormat`
// by replacing the `version` variable in the template by `(.+)`.
// The `tagFormat` is compiled with space as the `version` as it's an invalid tag character,
// so it's guaranteed to no be present in the `tagFormat`.
const tagRegexp = `^${escapeRegExp(template(tagFormat)({version: ' '})).replace(' ', '(.[^@]+)@?(.+)?')}`;
const tags = (await getTags({cwd, env}))
.map(tag => {
const [, version, channel] = tag.match(tagRegexp) || [];
return {gitTag: tag, version, channel};
})
.filter(({version}) => version && semver.valid(semver.clean(version)));

debug('found tags: %o', tags);

return pReduce(
branches,
async (branches, branch) => {
const branchTags = await pReduce(
tags,
async (tags, {gitTag, ...rest}) =>
(await isRefInHistory(gitTag, branch.name, {cwd, env}))
? [...tags, {...rest, gitTag, gitHead: await getTagHead(gitTag, {cwd, env})}]
: tags,
[]
const branchTags = await Promise.all(
(await getTags(branch.name, {cwd, env}))
.map(tag => {
const [, version, channel] = tag.match(tagRegexp) || [];
return {gitTag: tag, version, channel};
})
.filter(({version}) => version && semver.valid(semver.clean(version)))
.map(async ({gitTag, ...rest}) => ({gitTag, gitHead: await getTagHead(gitTag, {cwd, env}), ...rest}))
);

debug('found tags for branch %s: %o', branch.name, branchTags);
Expand Down
7 changes: 4 additions & 3 deletions lib/git.js
Expand Up @@ -22,15 +22,16 @@ async function getTagHead(tagName, execaOpts) {
}

/**
* Get all the repository tags.
* Get all the tags for a given branch.
*
* @param {String} branch The branch for which to retrieve the tags.
* @param {Object} [execaOpts] Options to pass to `execa`.
*
* @return {Array<String>} List of git tags.
* @throws {Error} If the `git` command fails.
*/
async function getTags(execaOpts) {
return (await execa('git', ['tag'], execaOpts)).stdout
async function getTags(branch, execaOpts) {
return (await execa('git', ['tag', '--merged', branch], execaOpts)).stdout
.split('\n')
.map(tag => tag.trim())
.filter(Boolean);
Expand Down
6 changes: 3 additions & 3 deletions test/branches/get-tags.test.js
Expand Up @@ -109,15 +109,15 @@ test('Return branches with and empty tags array if no valid tag is found', async
await gitCommits(['Third'], {cwd});
await gitTagVersion('v3.0', undefined, {cwd});

const result = await getTags({cwd, options: {tagFormat: `prefix@v\${version}`}}, [{name: 'master'}, {name: 'next'}]);
const result = await getTags({cwd, options: {tagFormat: `prefix@v\${version}`}}, [{name: 'master'}]);

t.deepEqual(result, [{name: 'master', tags: []}, {name: 'next', tags: []}]);
t.deepEqual(result, [{name: 'master', tags: []}]);
});

test('Return branches with and empty tags array if no valid tag is found in history of configured branches', async t => {
const {cwd} = await gitRepo();
await gitCommits(['First'], {cwd});
await gitCheckout('other-branch', true, {cwd});
await gitCheckout('next', true, {cwd});
await gitCommits(['Second'], {cwd});
await gitTagVersion('v1.0.0', undefined, {cwd});
await gitTagVersion('v1.0.0@next', undefined, {cwd});
Expand Down
4 changes: 2 additions & 2 deletions test/git.test.js
Expand Up @@ -91,7 +91,7 @@ test('Fetch all tags on a detached head repository', async t => {

await fetch(repositoryUrl, 'master', {cwd});

t.deepEqual((await getTags({cwd})).sort(), ['v1.0.0', 'v1.0.1', 'v1.1.0'].sort());
t.deepEqual((await getTags('master', {cwd})).sort(), ['v1.0.0', 'v1.0.1', 'v1.1.0'].sort());
});

test('Verify if the commit `sha` is in the direct history of the current branch', async t => {
Expand Down Expand Up @@ -243,7 +243,7 @@ test('Return falsy for invalid tag names', async t => {
test('Throws error if obtaining the tags fails', async t => {
const cwd = tempy.directory();

await t.throwsAsync(getTags({cwd}));
await t.throwsAsync(getTags('master', {cwd}));
});

test('Return "true" if repository is up to date', async t => {
Expand Down

0 comments on commit cffe9a8

Please sign in to comment.