Skip to content

Commit 7eb2b3f

Browse files
committed
fix: glob assets before running git ls-files
The command `git ls-files -m -o` seems to behave differently based on the Git version used. In addition it fails on certain Git versions when `.git` or `.gitmodules` are included in the project dependencies. This change glob the `assets` first, avoiding to run `git ls-files -m -o` on the entire repo.
1 parent e097ee7 commit 7eb2b3f

11 files changed

+242
-111
lines changed

lib/git.js

+8-7
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@ const execa = require('execa');
22
const debug = require('debug')('semantic-release:git');
33

44
/**
5-
* Retrieve the list of files modified on the local repository.
5+
* Filter files modified on the local repository.
66
*
7+
* @param {Array<String>} files List of file paths to filter.
78
* @param {Object} [execaOpts] Options to pass to `execa`.
89
*
910
* @return {Array<String>} Array of modified files path.
1011
*/
11-
async function getModifiedFiles(execaOpts) {
12-
return (await execa.stdout('git', ['ls-files', '-m', '-o'], execaOpts))
12+
async function filterModifiedFiles(files, execaOpts) {
13+
return (await execa.stdout('git', ['ls-files', '-m', '-o', ...files], execaOpts))
1314
.split('\n')
14-
.map(tag => tag.trim())
15-
.filter(tag => Boolean(tag));
15+
.map(file => file.trim())
16+
.filter(file => Boolean(file));
1617
}
1718

1819
/**
@@ -22,7 +23,7 @@ async function getModifiedFiles(execaOpts) {
2223
* @param {Object} [execaOpts] Options to pass to `execa`.
2324
*/
2425
async function add(files, execaOpts) {
25-
const shell = await execa('git', ['add', '--force', '--ignore-errors'].concat(files), {...execaOpts, reject: false});
26+
const shell = await execa('git', ['add', '--force', '--ignore-errors', ...files], {...execaOpts, reject: false});
2627
debug('add file to git index', shell);
2728
}
2829

@@ -62,4 +63,4 @@ async function gitHead(execaOpts) {
6263
return execa.stdout('git', ['rev-parse', 'HEAD'], execaOpts);
6364
}
6465

65-
module.exports = {getModifiedFiles, add, gitHead, commit, push};
66+
module.exports = {filterModifiedFiles, add, gitHead, commit, push};

lib/glob-assets.js

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
const path = require('path');
2+
const {isPlainObject, castArray, uniq} = require('lodash');
3+
const dirGlob = require('dir-glob');
4+
const globby = require('globby');
5+
const debug = require('debug')('semantic-release:github');
6+
7+
const filesTransform = (files, cwd, transform) =>
8+
files.map(file => `${file.startsWith('!') ? '!' : ''}${transform(cwd, file.startsWith('!') ? file.slice(1) : file)}`);
9+
10+
module.exports = async ({cwd}, assets) =>
11+
uniq(
12+
[].concat(
13+
...(await Promise.all(
14+
assets.map(async asset => {
15+
// Wrap single glob definition in Array
16+
let glob = castArray(isPlainObject(asset) ? asset.path : asset);
17+
// TODO Temporary workaround for https://github.com/kevva/dir-glob/issues/7 and https://github.com/mrmlnc/fast-glob/issues/47
18+
glob = uniq([
19+
...filesTransform(await dirGlob(filesTransform(glob, cwd, path.resolve)), cwd, path.relative),
20+
...glob,
21+
]);
22+
23+
// Skip solo negated pattern (avoid to include every non js file with `!**/*.js`)
24+
if (glob.length <= 1 && glob[0].startsWith('!')) {
25+
debug(
26+
'skipping the negated glob %o as its alone in its group and would retrieve a large amount of files',
27+
glob[0]
28+
);
29+
return [];
30+
}
31+
32+
const globbed = await globby(glob, {
33+
cwd,
34+
expandDirectories: true,
35+
gitignore: false,
36+
dot: true,
37+
onlyFiles: false,
38+
});
39+
40+
return globbed.length > 0 ? globbed : [];
41+
})
42+
))
43+
)
44+
);

lib/prepare.js

+32-86
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,8 @@
1-
const path = require('path');
2-
const {pathExists} = require('fs-extra');
3-
const {isUndefined, isPlainObject, isArray, template, castArray, uniq} = require('lodash');
4-
const micromatch = require('micromatch');
5-
const dirGlob = require('dir-glob');
6-
const pReduce = require('p-reduce');
1+
const {template} = require('lodash');
72
const debug = require('debug')('semantic-release:git');
83
const resolveConfig = require('./resolve-config');
9-
const {getModifiedFiles, add, commit, push} = require('./git');
10-
11-
const CHANGELOG = 'CHANGELOG.md';
12-
const PACKAGE_JSON = 'package.json';
13-
const PACKAGE_LOCK_JSON = 'package-lock.json';
14-
const SHRINKWRAP_JSON = 'npm-shrinkwrap.json';
15-
16-
// TODO Temporary workaround for https://github.com/kevva/dir-glob/issues/7
17-
const resolvePaths = (files, cwd) =>
18-
files.map(
19-
file => `${file.startsWith('!') ? '!' : ''}${path.resolve(cwd, file.startsWith('!') ? file.slice(1) : file)}`
20-
);
4+
const globAssets = require('./glob-assets.js');
5+
const {filterModifiedFiles, add, commit, push} = require('./git');
216

227
/**
238
* Prepare a release commit including configurable files.
@@ -31,75 +16,36 @@ const resolvePaths = (files, cwd) =>
3116
* @param {Object} context.nextRelease The next release.
3217
* @param {Object} logger Global logger.
3318
*/
34-
module.exports = async (
35-
pluginConfig,
36-
{env, cwd, options: {branch, repositoryUrl}, lastRelease, nextRelease, logger}
37-
) => {
19+
module.exports = async (pluginConfig, context) => {
20+
const {
21+
env,
22+
cwd,
23+
options: {branch, repositoryUrl},
24+
lastRelease,
25+
nextRelease,
26+
logger,
27+
} = context;
3828
const {message, assets} = resolveConfig(pluginConfig, logger);
39-
const patterns = [];
40-
const modifiedFiles = await getModifiedFiles({env, cwd});
41-
const changelogPath = path.resolve(cwd, CHANGELOG);
42-
const pkgPath = path.resolve(cwd, PACKAGE_JSON);
43-
const pkgLockPath = path.resolve(cwd, PACKAGE_LOCK_JSON);
44-
const shrinkwrapPath = path.resolve(cwd, SHRINKWRAP_JSON);
45-
46-
if (isUndefined(assets) && (await pathExists(changelogPath))) {
47-
logger.log('Add %s to the release commit', changelogPath);
48-
patterns.push(changelogPath);
49-
}
50-
if (isUndefined(assets) && (await pathExists(pkgPath))) {
51-
logger.log('Add %s to the release commit', pkgPath);
52-
patterns.push(pkgPath);
53-
}
54-
if (isUndefined(assets) && (await pathExists(pkgLockPath))) {
55-
logger.log('Add %s to the release commit', pkgLockPath);
56-
patterns.push(pkgLockPath);
57-
}
58-
if (isUndefined(assets) && (await pathExists(shrinkwrapPath))) {
59-
logger.log('Add %s to the release commit', shrinkwrapPath);
60-
patterns.push(shrinkwrapPath);
61-
}
62-
63-
patterns.push(
64-
...(assets || []).map(pattern => (!isArray(pattern) && isPlainObject(pattern) ? pattern.path : pattern))
65-
);
6629

67-
const filesToCommit = uniq(
68-
await pReduce(
69-
patterns,
70-
async (result, pattern) => {
71-
const glob = castArray(pattern);
72-
let nonegate;
73-
// Skip solo negated pattern (avoid to include every non js file with `!**/*.js`)
74-
if (glob.length <= 1 && glob[0].startsWith('!')) {
75-
nonegate = true;
76-
debug(
77-
'skipping the negated glob %o as its alone in its group and would retrieve a large amount of files ',
78-
glob[0]
79-
);
80-
}
81-
result.push(
82-
...micromatch(resolvePaths(modifiedFiles, cwd), await dirGlob(resolvePaths(glob, cwd)), {dot: true, nonegate})
83-
);
84-
return result;
85-
},
86-
[]
87-
)
88-
);
89-
90-
if (filesToCommit.length > 0) {
91-
logger.log('Found %d file(s) to commit', filesToCommit.length);
92-
await add(filesToCommit, {env, cwd});
93-
debug('commited files: %o', filesToCommit);
94-
await commit(
95-
message
96-
? template(message)({branch, lastRelease, nextRelease})
97-
: `chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}`,
98-
{env, cwd}
99-
);
30+
if (assets && assets.length > 0) {
31+
const globbedAssets = await globAssets(context, assets);
32+
debug('globed assets: %o', globbedAssets);
33+
34+
const filesToCommit = await filterModifiedFiles(globbedAssets, {cwd, env});
35+
36+
if (filesToCommit.length > 0) {
37+
logger.log('Found %d file(s) to commit', filesToCommit.length);
38+
await add(filesToCommit, {env, cwd});
39+
debug('commited files: %o', filesToCommit);
40+
await commit(
41+
message
42+
? template(message)({branch, lastRelease, nextRelease})
43+
: `chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}`,
44+
{env, cwd}
45+
);
46+
}
47+
48+
await push(repositoryUrl, branch, {env, cwd});
49+
logger.log('Prepared Git release: %s', nextRelease.gitTag);
10050
}
101-
102-
logger.log('Creating tag %s', nextRelease.gitTag);
103-
await push(repositoryUrl, branch, {env, cwd});
104-
logger.log('Prepared Git release: %s', nextRelease.gitTag);
10551
};

lib/resolve-config.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1-
const {castArray} = require('lodash');
1+
const {isUndefined, castArray} = require('lodash');
22

3-
module.exports = ({assets, message}) => ({assets: assets ? castArray(assets) : assets, message});
3+
module.exports = ({assets, message}) => ({
4+
assets: isUndefined(assets)
5+
? ['CHANGELOG.md', 'package.json', 'package-lock.json', 'npm-shrinkwrap.json']
6+
: assets
7+
? castArray(assets)
8+
: assets,
9+
message,
10+
});

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"dir-glob": "^2.0.0",
2323
"execa": "^0.10.0",
2424
"fs-extra": "^7.0.0",
25+
"globby": "^8.0.1",
2526
"lodash": "^4.17.4",
2627
"micromatch": "^3.1.4",
2728
"p-reduce": "^1.0.0"

test/fixtures/files/.dotfile

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dotfile content

test/fixtures/files/upload.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Upload file content

test/fixtures/files/upload_other.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Upload_other file content

test/git.test.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import path from 'path';
22
import test from 'ava';
33
import {outputFile, appendFile} from 'fs-extra';
4-
import {add, getModifiedFiles, commit, gitHead, push} from '../lib/git';
4+
import {add, filterModifiedFiles, commit, gitHead, push} from '../lib/git';
55
import {gitRepo, gitCommits, gitGetCommits, gitStaged, gitRemoteHead} from './helpers/git-utils';
66

77
test('Add file to index', async t => {
@@ -15,7 +15,7 @@ test('Add file to index', async t => {
1515
await t.deepEqual(await gitStaged({cwd}), ['file1.js']);
1616
});
1717

18-
test('Get the modified files, including files in .gitignore but including untracked ones', async t => {
18+
test('Filter modified files, including files in .gitignore and untracked ones', async t => {
1919
// Create a git repository, set the current working directory at the root of the repo
2020
const {cwd} = await gitRepo();
2121
// Create files
@@ -35,16 +35,16 @@ test('Get the modified files, including files in .gitignore but including untrac
3535
await outputFile(path.resolve(cwd, 'file4.js'), 'Test content');
3636

3737
await t.deepEqual(
38-
(await getModifiedFiles({cwd})).sort(),
39-
['file4.js', 'file3.js', 'dir/file2.js', 'file1.js'].sort()
38+
(await filterModifiedFiles(['file1.js', 'dir/file2.js', 'file3.js', 'file4.js'], {cwd})).sort(),
39+
['file1.js', 'dir/file2.js', 'file3.js', 'file4.js'].sort()
4040
);
4141
});
4242

4343
test('Returns [] if there is no modified files', async t => {
4444
// Create a git repository, set the current working directory at the root of the repo
4545
const {cwd} = await gitRepo();
4646

47-
await t.deepEqual(await getModifiedFiles({cwd}), []);
47+
await t.deepEqual(await filterModifiedFiles(['file1.js', 'file2.js'], {cwd}), []);
4848
});
4949

5050
test('Commit added files', async t => {

0 commit comments

Comments
 (0)