From f59d20cf435ea12f9c15383b7ce375c25a45edd6 Mon Sep 17 00:00:00 2001 From: Pierre Vanduynslager Date: Mon, 4 Nov 2019 18:57:11 -0500 Subject: [PATCH] fix: include deleted files in release commit BREAKING CHANGE: Require Git 2.7.1 or higher --- lib/git.js | 17 ++-- lib/glob-assets.js | 37 ------- lib/prepare.js | 56 +++++++---- package.json | 3 +- test/fixtures/files/.dotfile | 1 - test/fixtures/files/upload.txt | 1 - test/fixtures/files/upload_other.txt | 1 - test/git.test.js | 18 +--- test/glob-assets.test.js | 138 --------------------------- test/helpers/git-utils.js | 21 ++++ test/prepare.test.js | 28 +++++- 11 files changed, 97 insertions(+), 224 deletions(-) delete mode 100644 lib/glob-assets.js delete mode 100644 test/fixtures/files/.dotfile delete mode 100644 test/fixtures/files/upload.txt delete mode 100644 test/fixtures/files/upload_other.txt delete mode 100644 test/glob-assets.test.js diff --git a/lib/git.js b/lib/git.js index 8340946d..7598bf9a 100644 --- a/lib/git.js +++ b/lib/git.js @@ -2,20 +2,17 @@ const execa = require('execa'); const debug = require('debug')('semantic-release:git'); /** - * Filter files modified on the local repository. + * Retrieve the list of files modified on the local repository. * - * @param {Array} files List of file paths to filter. * @param {Object} [execaOpts] Options to pass to `execa`. * * @return {Array} Array of modified files path. */ -async function filterModifiedFiles(files, execaOpts) { - return files.length > 0 - ? (await execa('git', ['ls-files', '-m', '-o', ...files], execaOpts)).stdout - .split('\n') - .map(file => file.trim()) - .filter(file => Boolean(file)) - : []; +async function getModifiedFiles(execaOpts) { + return (await execa('git', ['ls-files', '-m', '-o'], execaOpts)).stdout + .split('\n') + .map(file => file.trim()) + .filter(file => Boolean(file)); } /** @@ -65,4 +62,4 @@ async function gitHead(execaOpts) { return (await execa('git', ['rev-parse', 'HEAD'], execaOpts)).stdout; } -module.exports = {filterModifiedFiles, add, gitHead, commit, push}; +module.exports = {getModifiedFiles, add, gitHead, commit, push}; diff --git a/lib/glob-assets.js b/lib/glob-assets.js deleted file mode 100644 index 1a760560..00000000 --- a/lib/glob-assets.js +++ /dev/null @@ -1,37 +0,0 @@ -const {isPlainObject, castArray, uniq} = require('lodash'); -const dirGlob = require('dir-glob'); -const globby = require('globby'); -const debug = require('debug')('semantic-release:github'); - -module.exports = async ({cwd}, assets) => - uniq( - [].concat( - ...(await Promise.all( - assets.map(async asset => { - // Wrap single glob definition in Array - let glob = castArray(isPlainObject(asset) ? asset.path : asset); - // TODO Temporary workaround for https://github.com/mrmlnc/fast-glob/issues/47 - glob = uniq([...(await dirGlob(glob, {cwd})), ...glob]); - - // Skip solo negated pattern (avoid to include every non js file with `!**/*.js`) - if (glob.length <= 1 && glob[0].startsWith('!')) { - debug( - 'skipping the negated glob %o as its alone in its group and would retrieve a large amount of files', - glob[0] - ); - return []; - } - - const globbed = await globby(glob, { - cwd, - expandDirectories: false, // TODO Temporary workaround for https://github.com/mrmlnc/fast-glob/issues/47 - gitignore: false, - dot: true, - onlyFiles: false, - }); - - return globbed.length > 0 ? globbed : []; - }) - )) - ) - ); diff --git a/lib/prepare.js b/lib/prepare.js index 0e7a7939..0e85b005 100644 --- a/lib/prepare.js +++ b/lib/prepare.js @@ -1,8 +1,10 @@ -const {template} = require('lodash'); +const {isPlainObject, isArray, template, castArray, uniq} = require('lodash'); +const micromatch = require('micromatch'); +const dirGlob = require('dir-glob'); +const pReduce = require('p-reduce'); const debug = require('debug')('semantic-release:git'); const resolveConfig = require('./resolve-config'); -const globAssets = require('./glob-assets.js'); -const {filterModifiedFiles, add, commit, push} = require('./git'); +const {getModifiedFiles, add, commit, push} = require('./git'); /** * Prepare a release commit including configurable files. @@ -28,24 +30,42 @@ module.exports = async (pluginConfig, context) => { } = context; const {message, assets} = resolveConfig(pluginConfig, logger); - if (assets && assets.length > 0) { - const globbedAssets = await globAssets(context, assets); - debug('globed assets: %o', globbedAssets); + const modifiedFiles = await getModifiedFiles({env, cwd}); - const filesToCommit = await filterModifiedFiles(globbedAssets, {cwd, env}); + const filesToCommit = uniq( + await pReduce( + assets.map(asset => (!isArray(asset) && isPlainObject(asset) ? asset.path : asset)), + async (result, asset) => { + const glob = castArray(asset); + let nonegate; + // Skip solo negated pattern (avoid to include every non js file with `!**/*.js`) + if (glob.length <= 1 && glob[0].startsWith('!')) { + nonegate = true; + debug( + 'skipping the negated glob %o as its alone in its group and would retrieve a large amount of files ', + glob[0] + ); + } - if (filesToCommit.length > 0) { - logger.log('Found %d file(s) to commit', filesToCommit.length); - await add(filesToCommit, {env, cwd}); - debug('commited files: %o', filesToCommit); - await commit( - message - ? template(message)({branch: branch.name, lastRelease, nextRelease}) - : `chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}`, - {env, cwd} - ); - } + return [ + ...result, + ...micromatch(modifiedFiles, await dirGlob(glob, {cwd}), {dot: true, nonegate, cwd, expand: true}), + ]; + }, + [] + ) + ); + if (filesToCommit.length > 0) { + logger.log('Found %d file(s) to commit', filesToCommit.length); + await add(filesToCommit, {env, cwd}); + debug('commited files: %o', filesToCommit); + await commit( + message + ? template(message)({branch: branch.name, lastRelease, nextRelease}) + : `chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}`, + {env, cwd} + ); await push(repositoryUrl, branch.name, {env, cwd}); logger.log('Prepared Git release: %s', nextRelease.gitTag); } diff --git a/package.json b/package.json index 5b186466..d31f2a32 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,6 @@ "debug": "^4.0.0", "dir-glob": "^3.0.0", "execa": "^3.0.0", - "fs-extra": "^8.0.0", - "globby": "^10.0.0", "lodash": "^4.17.4", "micromatch": "^4.0.0", "p-reduce": "^2.0.0" @@ -35,6 +33,7 @@ "clear-module": "^4.0.0", "codecov": "^3.0.0", "file-url": "^3.0.0", + "fs-extra": "^8.0.0", "get-stream": "^5.0.0", "git-log-parser": "^1.2.0", "nyc": "^14.0.0", diff --git a/test/fixtures/files/.dotfile b/test/fixtures/files/.dotfile deleted file mode 100644 index 5b0e63c7..00000000 --- a/test/fixtures/files/.dotfile +++ /dev/null @@ -1 +0,0 @@ -dotfile content diff --git a/test/fixtures/files/upload.txt b/test/fixtures/files/upload.txt deleted file mode 100644 index 5e2dd259..00000000 --- a/test/fixtures/files/upload.txt +++ /dev/null @@ -1 +0,0 @@ -Upload file content \ No newline at end of file diff --git a/test/fixtures/files/upload_other.txt b/test/fixtures/files/upload_other.txt deleted file mode 100644 index 8995f768..00000000 --- a/test/fixtures/files/upload_other.txt +++ /dev/null @@ -1 +0,0 @@ -Upload_other file content diff --git a/test/git.test.js b/test/git.test.js index 6c5d927b..1fff3ea0 100644 --- a/test/git.test.js +++ b/test/git.test.js @@ -1,7 +1,7 @@ import path from 'path'; import test from 'ava'; import {outputFile, appendFile} from 'fs-extra'; -import {add, filterModifiedFiles, commit, gitHead, push} from '../lib/git'; +import {add, getModifiedFiles, commit, gitHead, push} from '../lib/git'; import {gitRepo, gitCommits, gitGetCommits, gitStaged, gitRemoteHead} from './helpers/git-utils'; test('Add file to index', async t => { @@ -15,7 +15,7 @@ test('Add file to index', async t => { await t.deepEqual(await gitStaged({cwd}), ['file1.js']); }); -test('Filter modified files, including files in .gitignore and untracked ones', async t => { +test('Get the modified files, including files in .gitignore but including untracked ones', async t => { // Create a git repository, set the current working directory at the root of the repo const {cwd} = await gitRepo(); // Create files @@ -35,7 +35,7 @@ test('Filter modified files, including files in .gitignore and untracked ones', await outputFile(path.resolve(cwd, 'file4.js'), 'Test content'); await t.deepEqual( - (await filterModifiedFiles(['file1.js', 'dir/file2.js', 'file3.js', 'file4.js'], {cwd})).sort(), + (await getModifiedFiles({cwd})).sort(), ['file1.js', 'dir/file2.js', 'file3.js', 'file4.js'].sort() ); }); @@ -44,17 +44,7 @@ test('Returns [] if there is no modified files', async t => { // Create a git repository, set the current working directory at the root of the repo const {cwd} = await gitRepo(); - await t.deepEqual(await filterModifiedFiles(['file1.js', 'file2.js'], {cwd}), []); -}); - -test('Returns [] if there is no files for which to check modification', async t => { - // Create a git repository, set the current working directory at the root of the repo - const {cwd} = await gitRepo(); - // Create files - await outputFile(path.resolve(cwd, 'file1.js'), ''); - await outputFile(path.resolve(cwd, 'dir/file2.js'), ''); - - await t.deepEqual(await filterModifiedFiles([], {cwd}), []); + await t.deepEqual(await getModifiedFiles({cwd}), []); }); test('Commit added files', async t => { diff --git a/test/glob-assets.test.js b/test/glob-assets.test.js deleted file mode 100644 index 883e2434..00000000 --- a/test/glob-assets.test.js +++ /dev/null @@ -1,138 +0,0 @@ -import path from 'path'; -import test from 'ava'; -import {copy, ensureDir} from 'fs-extra'; -import tempy from 'tempy'; -import globAssets from '../lib/glob-assets'; - -const fixtures = 'test/fixtures/files'; - -test('Retrieve file from single path', async t => { - const cwd = tempy.directory(); - await copy(fixtures, cwd); - const globbedAssets = await globAssets({cwd}, ['upload.txt']); - - t.deepEqual(globbedAssets, ['upload.txt']); -}); - -test('Retrieve multiple files from path', async t => { - const cwd = tempy.directory(); - await copy(fixtures, cwd); - const globbedAssets = await globAssets({cwd}, ['upload.txt', 'upload_other.txt']); - - t.deepEqual(globbedAssets.sort(), ['upload_other.txt', 'upload.txt'].sort()); -}); - -test('Retrieve multiple files from Object', async t => { - const cwd = tempy.directory(); - await copy(fixtures, cwd); - const globbedAssets = await globAssets({cwd}, [ - {path: 'upload.txt', name: 'upload_name', label: 'Upload label'}, - 'upload_other.txt', - ]); - - t.deepEqual(globbedAssets.sort(), ['upload.txt', 'upload_other.txt'].sort()); -}); - -test('Retrieve multiple files without duplicates', async t => { - const cwd = tempy.directory(); - await copy(fixtures, cwd); - const globbedAssets = await globAssets({cwd}, [ - 'upload_other.txt', - 'upload.txt', - 'upload_other.txt', - 'upload.txt', - 'upload.txt', - 'upload_other.txt', - ]); - - t.deepEqual(globbedAssets.sort(), ['upload_other.txt', 'upload.txt'].sort()); -}); - -test('Retrieve file from single glob', async t => { - const cwd = tempy.directory(); - await copy(fixtures, cwd); - const globbedAssets = await globAssets({cwd}, ['upload.*']); - - t.deepEqual(globbedAssets, ['upload.txt']); -}); - -test('Retrieve multiple files from single glob', async t => { - const cwd = tempy.directory(); - await copy(fixtures, cwd); - const globbedAssets = await globAssets({cwd}, ['*.txt']); - - t.deepEqual(globbedAssets.sort(), ['upload_other.txt', 'upload.txt'].sort()); -}); - -test('Accept glob array with one value', async t => { - const cwd = tempy.directory(); - await copy(fixtures, cwd); - const globbedAssets = await globAssets({cwd}, [['*load.txt'], ['*_other.txt']]); - - t.deepEqual(globbedAssets.sort(), ['upload_other.txt', 'upload.txt'].sort()); -}); - -test('Exclude globs that resolve to no files', async t => { - const cwd = tempy.directory(); - await copy(fixtures, cwd); - const globbedAssets = await globAssets({cwd}, [['upload.txt', '!upload.txt']]); - - t.deepEqual(globbedAssets, []); -}); - -test('Accept glob array with one value for missing files', async t => { - const cwd = tempy.directory(); - await copy(fixtures, cwd); - const globbedAssets = await globAssets({cwd}, [['*missing.txt'], ['*_other.txt']]); - - t.deepEqual(globbedAssets.sort(), ['upload_other.txt'].sort()); -}); - -test('Include dotfiles', async t => { - const cwd = tempy.directory(); - await copy(fixtures, cwd); - const globbedAssets = await globAssets({cwd}, ['.dot*']); - - t.deepEqual(globbedAssets, ['.dotfile']); -}); - -test('Ingnore single negated glob', async t => { - const cwd = tempy.directory(); - await copy(fixtures, cwd); - const globbedAssets = await globAssets({cwd}, ['!*.txt']); - - t.deepEqual(globbedAssets, []); -}); - -test('Ingnore single negated glob in Object', async t => { - const cwd = tempy.directory(); - await copy(fixtures, cwd); - const globbedAssets = await globAssets({cwd}, [{path: '!*.txt'}]); - - t.deepEqual(globbedAssets, []); -}); - -test('Accept negated globs', async t => { - const cwd = tempy.directory(); - await copy(fixtures, cwd); - const globbedAssets = await globAssets({cwd}, [['*.txt', '!**/*_other.txt']]); - - t.deepEqual(globbedAssets, ['upload.txt']); -}); - -test('Expand directories', async t => { - const cwd = tempy.directory(); - await copy(fixtures, path.resolve(cwd, 'dir')); - const globbedAssets = await globAssets({cwd}, [['dir']]); - - t.deepEqual(globbedAssets.sort(), ['dir', 'dir/upload_other.txt', 'dir/upload.txt', 'dir/.dotfile'].sort()); -}); - -test('Include empty directory as defined', async t => { - const cwd = tempy.directory(); - await copy(fixtures, cwd); - await ensureDir(path.resolve(cwd, 'empty')); - const globbedAssets = await globAssets({cwd}, [['empty']]); - - t.deepEqual(globbedAssets, ['empty']); -}); diff --git a/test/helpers/git-utils.js b/test/helpers/git-utils.js index bc06b373..42bfae04 100644 --- a/test/helpers/git-utils.js +++ b/test/helpers/git-utils.js @@ -186,3 +186,24 @@ export async function gitCommitedFiles(ref, execaOpts) { .split('\n') .filter(file => Boolean(file)); } + +/** + * Add a list of file to the Git index. + * + * @param {Array} files Array of files path to add to the index. + * @param {Object} [execaOpts] Options to pass to `execa`. + */ +export async function gitAdd(files, execaOpts) { + await execa('git', ['add', '--force', '--ignore-errors', ...files], {...execaOpts}); +} + +/** + * Push to the remote repository. + * + * @param {String} repositoryUrl The remote repository URL. + * @param {String} branch The branch to push. + * @param {Object} [execaOpts] Options to pass to `execa`. + */ +export async function gitPush(repositoryUrl, branch, execaOpts) { + await execa('git', ['push', '--tags', repositoryUrl, `HEAD:${branch}`], execaOpts); +} diff --git a/test/prepare.test.js b/test/prepare.test.js index f725e6aa..3a9b6b66 100644 --- a/test/prepare.test.js +++ b/test/prepare.test.js @@ -1,9 +1,9 @@ import path from 'path'; import test from 'ava'; -import {outputFile} from 'fs-extra'; +import {outputFile, remove} from 'fs-extra'; import {stub} from 'sinon'; import prepare from '../lib/prepare'; -import {gitRepo, gitGetCommits, gitCommitedFiles} from './helpers/git-utils'; +import {gitRepo, gitGetCommits, gitCommitedFiles, gitAdd, gitCommits, gitPush} from './helpers/git-utils'; test.beforeEach(t => { // Stub the logger functions @@ -190,6 +190,30 @@ test('Commit files matching the patterns in "assets", including dot files', asyn t.deepEqual(t.context.log.args[0], ['Found %d file(s) to commit', 1]); }); +test('Include deleted files in release commit', async t => { + const {cwd, repositoryUrl} = await gitRepo(true); + const pluginConfig = { + assets: ['file1.js'], + }; + const branch = {name: 'master'}; + const options = {repositoryUrl}; + const env = {}; + const lastRelease = {}; + const nextRelease = {version: '2.0.0', gitTag: 'v2.0.0'}; + await outputFile(path.resolve(cwd, 'file1.js'), 'Test content'); + await outputFile(path.resolve(cwd, 'file2.js'), 'Test content'); + + await gitAdd(['file1.js', 'file2.js'], {cwd, env}); + await gitCommits(['Add file1.js and file2.js'], {cwd, env}); + await gitPush(repositoryUrl, 'master', {cwd, env}); + + await remove(path.resolve(cwd, 'file1.js')); + await prepare(pluginConfig, {cwd, env, options, branch, lastRelease, nextRelease, logger: t.context.logger}); + + t.deepEqual((await gitCommitedFiles('HEAD', {cwd, env})).sort(), ['file1.js'].sort()); + t.deepEqual(t.context.log.args[0], ['Found %d file(s) to commit', 1]); +}); + test('Set the commit author and committer name/email based on environment variables', async t => { const {cwd, repositoryUrl} = await gitRepo(true); const branch = {name: 'master'};