From 0ef151e11d52d9ae0d3e4c73d7f73104fc7ac25f Mon Sep 17 00:00:00 2001 From: Dmitrii Abramov Date: Mon, 3 Jul 2017 23:29:03 -0700 Subject: [PATCH] multiroot jest-change-files --- .../__tests__/jest_changed_files.test.js | 237 ++++++++++++++++++ integration_tests/utils.js | 3 + packages/jest-changed-files/package.json | 5 +- .../src/__tests__/git.test.js | 75 ------ .../src/__tests__/hg.test.js | 72 ------ packages/jest-changed-files/src/git.js | 101 ++++---- packages/jest-changed-files/src/hg.js | 100 ++++---- packages/jest-changed-files/src/index.js | 55 +++- packages/jest-changed-files/types.js | 23 ++ packages/jest-changed-files/yarn.lock | 7 + packages/jest-cli/src/search_source.js | 45 +--- 11 files changed, 437 insertions(+), 286 deletions(-) create mode 100644 integration_tests/__tests__/jest_changed_files.test.js delete mode 100644 packages/jest-changed-files/src/__tests__/git.test.js delete mode 100644 packages/jest-changed-files/src/__tests__/hg.test.js create mode 100644 packages/jest-changed-files/types.js create mode 100644 packages/jest-changed-files/yarn.lock diff --git a/integration_tests/__tests__/jest_changed_files.test.js b/integration_tests/__tests__/jest_changed_files.test.js new file mode 100644 index 000000000000..73ac913d2c6c --- /dev/null +++ b/integration_tests/__tests__/jest_changed_files.test.js @@ -0,0 +1,237 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +'use strict'; + +import {cleanup, run, writeFiles} from '../utils'; +import os from 'os'; +import path from 'path'; +import { + findRepos, + getChangedFilesForRoots, +} from '../../packages/jest-changed-files/src'; + +const skipOnWindows = require('skipOnWindows'); +skipOnWindows.suite(); + +const DIR = path.resolve(os.tmpdir(), 'jest_changed_files_test_dir'); + +const GIT = 'git -c user.name=jest_test -c user.email=jest_test@test.com'; +const HG = 'hg --config ui.username=jest_test'; + +beforeEach(() => cleanup(DIR)); +afterEach(() => cleanup(DIR)); + +test('gets hg SCM roots and dedups them', async () => { + writeFiles(DIR, { + 'first_repo/file1.txt': 'file1', + 'first_repo/nested_dir/file2.txt': 'file2', + 'first_repo/nested_dir/second_nested_dir/file3.txt': 'file3', + 'second_repo/file1.txt': 'file1', + 'second_repo/nested_dir/file2.txt': 'file2', + 'second_repo/nested_dir/second_nested_dir/file3.txt': 'file3', + }); + + run(`${HG} init`, path.resolve(DIR, 'first_repo')); + run(`${HG} init`, path.resolve(DIR, 'second_repo')); + + const roots = [ + '', + 'first_repo/nested_dir', + 'first_repo/nested_dir/second_nested_dir', + 'second_repo/nested_dir', + 'second_repo/nested_dir/second_nested_dir', + ].map(filename => path.resolve(DIR, filename)); + + const repos = await findRepos(roots); + expect(repos.git.size).toBe(0); + + const hgRepos = Array.from(repos.hg); + + // it's not possible to match the exact path because it will resolve + // differently on different platforms. + // NOTE: This test can break if you have a .hg repo initialized inside your + // os tmp directory. + expect(hgRepos).toHaveLength(2); + expect(hgRepos[0]).toMatch(/\/jest_changed_files_test_dir\/first_repo$/); + expect(hgRepos[1]).toMatch(/\/jest_changed_files_test_dir\/second_repo$/); +}); + +test('gets git SCM roots and dedups them', async () => { + writeFiles(DIR, { + 'first_repo/file1.txt': 'file1', + 'first_repo/nested_dir/file2.txt': 'file2', + 'first_repo/nested_dir/second_nested_dir/file3.txt': 'file3', + 'second_repo/file1.txt': 'file1', + 'second_repo/nested_dir/file2.txt': 'file2', + 'second_repo/nested_dir/second_nested_dir/file3.txt': 'file3', + }); + + run(`${GIT} init`, path.resolve(DIR, 'first_repo')); + run(`${GIT} init`, path.resolve(DIR, 'second_repo')); + + const roots = [ + '', + 'first_repo/nested_dir', + 'first_repo/nested_dir/second_nested_dir', + 'second_repo/nested_dir', + 'second_repo/nested_dir/second_nested_dir', + ].map(filename => path.resolve(DIR, filename)); + + const repos = await findRepos(roots); + expect(repos.hg.size).toBe(0); + const gitRepos = Array.from(repos.git); + + // it's not possible to match the exact path because it will resolve + // differently on different platforms. + // NOTE: This test can break if you have a .git repo initialized inside your + // os tmp directory. + expect(gitRepos).toHaveLength(2); + expect(gitRepos[0]).toMatch(/\/jest_changed_files_test_dir\/first_repo$/); + expect(gitRepos[1]).toMatch(/\/jest_changed_files_test_dir\/second_repo$/); +}); + +test('gets mixed git and hg SCM roots and dedups them', async () => { + writeFiles(DIR, { + 'first_repo/file1.txt': 'file1', + 'first_repo/nested_dir/file2.txt': 'file2', + 'first_repo/nested_dir/second_nested_dir/file3.txt': 'file3', + 'second_repo/file1.txt': 'file1', + 'second_repo/nested_dir/file2.txt': 'file2', + 'second_repo/nested_dir/second_nested_dir/file3.txt': 'file3', + }); + + run(`${GIT} init`, path.resolve(DIR, 'first_repo')); + run(`${HG} init`, path.resolve(DIR, 'second_repo')); + + const roots = [ + '', + 'first_repo/nested_dir', + 'first_repo/nested_dir/second_nested_dir', + 'second_repo/nested_dir', + 'second_repo/nested_dir/second_nested_dir', + ].map(filename => path.resolve(DIR, filename)); + + const repos = await findRepos(roots); + const hgRepos = Array.from(repos.hg); + const gitRepos = Array.from(repos.git); + + // NOTE: This test can break if you have a .git or .hg repo initialized + // inside your os tmp directory. + expect(gitRepos).toHaveLength(1); + expect(hgRepos).toHaveLength(1); + expect(gitRepos[0]).toMatch(/\/jest_changed_files_test_dir\/first_repo$/); + expect(hgRepos[0]).toMatch(/\/jest_changed_files_test_dir\/second_repo$/); +}); + +test('gets changed files for git', async () => { + writeFiles(DIR, { + 'file1.txt': 'file1', + 'nested_dir/file2.txt': 'file2', + 'nested_dir/second_nested_dir/file3.txt': 'file3', + }); + + run(`${GIT} init`, DIR); + + const roots = [ + '', + 'nested_dir', + 'nested_dir/second_nested_dir', + ].map(filename => path.resolve(DIR, filename)); + + let {changedFiles: files} = await getChangedFilesForRoots(roots, {}); + expect( + Array.from(files).map(filePath => path.basename(filePath)).sort(), + ).toEqual(['file1.txt', 'file2.txt', 'file3.txt']); + + run(`${GIT} add .`, DIR); + run(`${GIT} commit -m "test"`, DIR); + + ({changedFiles: files} = await getChangedFilesForRoots(roots, {})); + expect(Array.from(files)).toEqual([]); + + ({changedFiles: files} = await getChangedFilesForRoots(roots, { + lastCommit: true, + })); + expect( + Array.from(files).map(filePath => path.basename(filePath)).sort(), + ).toEqual(['file1.txt', 'file2.txt', 'file3.txt']); + + writeFiles(DIR, { + 'file1.txt': 'modified file1', + }); + + ({changedFiles: files} = await getChangedFilesForRoots(roots)); + expect( + Array.from(files).map(filePath => path.basename(filePath)).sort(), + ).toEqual(['file1.txt']); +}); + +test('gets changed files for hg', async () => { + if (process.env.CI) { + // Circle and Travis have very old version of hg (v2, and current + // version is v4.2) and its API changed since then and not compatible + // any more. Changing the SCM version on CIs is not trivial, so we'll just + // skip this test and run it only locally. + return; + } + writeFiles(DIR, { + 'file1.txt': 'file1', + 'nested_dir/file2.txt': 'file2', + 'nested_dir/second_nested_dir/file3.txt': 'file3', + }); + + run(`${HG} init`, DIR); + + const roots = [ + '', + 'nested_dir', + 'nested_dir/second_nested_dir', + ].map(filename => path.resolve(DIR, filename)); + + let {changedFiles: files} = await getChangedFilesForRoots(roots, {}); + expect( + Array.from(files).map(filePath => path.basename(filePath)).sort(), + ).toEqual(['file1.txt', 'file2.txt', 'file3.txt']); + + run(`${HG} add .`, DIR); + run(`${HG} commit -m "test"`, DIR); + + ({changedFiles: files} = await getChangedFilesForRoots(roots, {})); + expect(Array.from(files)).toEqual([]); + + ({changedFiles: files} = await getChangedFilesForRoots(roots, { + lastCommit: true, + })); + expect( + Array.from(files).map(filePath => path.basename(filePath)).sort(), + ).toEqual(['file1.txt', 'file2.txt', 'file3.txt']); + + writeFiles(DIR, { + 'file1.txt': 'modified file1', + }); + + ({changedFiles: files} = await getChangedFilesForRoots(roots)); + expect( + Array.from(files).map(filePath => path.basename(filePath)).sort(), + ).toEqual(['file1.txt']); + + run(`${HG} commit -m "test2"`, DIR); + + writeFiles(DIR, { + 'file4.txt': 'file4', + }); + + ({changedFiles: files} = await getChangedFilesForRoots(roots, { + withAncestor: true, + })); + // Returns files from current uncommited state + the last commit + expect( + Array.from(files).map(filePath => path.basename(filePath)).sort(), + ).toEqual(['file1.txt', 'file4.txt']); +}); diff --git a/integration_tests/utils.js b/integration_tests/utils.js index 0dda845eca52..4c1e638353f6 100644 --- a/integration_tests/utils.js +++ b/integration_tests/utils.js @@ -29,6 +29,9 @@ const run = (cmd, cwd) => { throw new Error(message); } + result.stdout = result.stdout && result.stdout.toString(); + result.stderr = result.stderr && result.stderr.toString(); + return result; }; diff --git a/packages/jest-changed-files/package.json b/packages/jest-changed-files/package.json index aabd6351f53e..e1d621580752 100644 --- a/packages/jest-changed-files/package.json +++ b/packages/jest-changed-files/package.json @@ -6,5 +6,8 @@ "url": "https://github.com/facebook/jest.git" }, "license": "BSD-3-Clause", - "main": "build/index.js" + "main": "build/index.js", + "dependencies": { + "throat": "^4.0.0" + } } diff --git a/packages/jest-changed-files/src/__tests__/git.test.js b/packages/jest-changed-files/src/__tests__/git.test.js deleted file mode 100644 index e71c2123c8e8..000000000000 --- a/packages/jest-changed-files/src/__tests__/git.test.js +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ -'use strict'; - -const os = require('os'); -const fs = require('fs'); -const path = require('path'); -const childProcess = require('child_process'); -const rimraf = require('rimraf'); -const mkdirp = require('mkdirp'); - -const tmpdir = path.resolve(os.tmpdir(), 'jest-changed-files-git'); -const tmpfile = path.resolve(tmpdir, Date.now() + '.js'); -const tmpdirNested = path.resolve(tmpdir, 'src'); -const tmpfileNested = path.resolve(tmpdirNested, Date.now() + '.js'); -const options = { - lastCommit: false, -}; - -describe('git', () => { - let git; - - beforeEach(() => { - git = require('../git'); - mkdirp.sync(tmpdirNested); - }); - - afterEach(() => rimraf.sync(tmpdir)); - - describe('isGitRepository', () => { - it('returns null for non git repo folder', () => { - return git.isGitRepository(tmpdir).then(res => { - expect(res).toBeNull(); - }); - }); - - it('returns dirname for git repo folder', () => { - childProcess.spawnSync('git', ['init', tmpdir]); - - return git.isGitRepository(tmpdir).then(res => { - if (process.platform === 'win32') { - // Git port on Win32 returns paths with "/" rather than "\" - res = res.replace(/\//g, '\\'); - } - expect(res).toContain(tmpdir); - }); - }); - }); - - describe('findChangedFiles', () => { - beforeEach(() => { - childProcess.spawnSync('git', ['init', tmpdir]); - }); - - it('returns an empty array for git repo folder without modified files', () => { - return git.findChangedFiles(tmpdir, options).then(res => { - expect(res).toEqual([]); - }); - }); - - it('returns an array of modified files for git repo folder', () => { - fs.writeFileSync(tmpfile); - fs.writeFileSync(tmpfileNested); - - return git.findChangedFiles(tmpdir, options).then(res => { - expect(res).toEqual([tmpfile, tmpfileNested]); - }); - }); - }); -}); diff --git a/packages/jest-changed-files/src/__tests__/hg.test.js b/packages/jest-changed-files/src/__tests__/hg.test.js deleted file mode 100644 index 84f208441491..000000000000 --- a/packages/jest-changed-files/src/__tests__/hg.test.js +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ -'use strict'; - -const os = require('os'); -const fs = require('fs'); -const path = require('path'); -const childProcess = require('child_process'); -const rimraf = require('rimraf'); -const mkdirp = require('mkdirp'); - -const tmpdir = path.resolve(os.tmpdir(), 'jest-changed-files-hg'); -const tmpfile = path.resolve(tmpdir, Date.now() + '.js'); -const tmpdirNested = path.resolve(tmpdir, 'src'); -const tmpfileNested = path.resolve(tmpdirNested, Date.now() + '.js'); -const options = { - lastCommit: false, -}; - -describe('hg', () => { - let hg; - - beforeEach(() => { - jest.resetModules(); - - hg = require('../hg'); - mkdirp.sync(tmpdirNested); - }); - - afterEach(() => rimraf.sync(tmpdir)); - - describe('isHGRepository', () => { - it('returns null for non hg repo folder', () => - hg.isHGRepository(tmpdir).then(res => { - expect(res).toBeNull(); - })); - - it('returns dirname for hg repo folder', () => { - childProcess.spawnSync('hg', ['init', tmpdir]); - - return hg.isHGRepository(tmpdir).then(res => { - expect(res).toContain(tmpdir); - }); - }); - }); - - describe('findChangedFiles', () => { - beforeEach(() => { - childProcess.spawnSync('hg', ['init', tmpdir]); - }); - - it('returns an empty array for hg repo folder without modified files', () => - hg.findChangedFiles(tmpdir, options).then(res => { - expect(res).toEqual([]); - })); - - it('returns an array of modified files for hg repo folder', () => { - fs.writeFileSync(tmpfile); - fs.writeFileSync(tmpfileNested); - childProcess.spawnSync('hg', ['add'], {cwd: tmpdir}); - - return hg.findChangedFiles(tmpdir, options).then(res => { - expect(res).toEqual([tmpfile, tmpfileNested]); - }); - }); - }); -}); diff --git a/packages/jest-changed-files/src/git.js b/packages/jest-changed-files/src/git.js index 31a0be596a21..97f3335b7643 100644 --- a/packages/jest-changed-files/src/git.js +++ b/packages/jest-changed-files/src/git.js @@ -9,65 +9,60 @@ */ import type {Path} from 'types/Config'; +import type {Options, SCMAdapter} from '../types'; import path from 'path'; import childProcess from 'child_process'; -type Options = {| - lastCommit?: boolean, - withAncestor?: boolean, -|}; - -function findChangedFiles( - cwd: string, - options?: Options, -): Promise> { - return new Promise((resolve, reject) => { - const args = - options && options.lastCommit - ? ['show', '--name-only', '--pretty=%b', 'HEAD'] - : ['ls-files', '--other', '--modified', '--exclude-standard']; - const child = childProcess.spawn('git', args, {cwd}); - let stdout = ''; - let stderr = ''; - child.stdout.on('data', data => (stdout += data)); - child.stderr.on('data', data => (stderr += data)); - child.on('error', e => reject(e)); - child.on('close', code => { - if (code === 0) { - stdout = stdout.trim(); - if (stdout === '') { - resolve([]); +const adapter: SCMAdapter = { + findChangedFiles: async ( + cwd: string, + options?: Options, + ): Promise> => { + return new Promise((resolve, reject) => { + const args = + options && options.lastCommit + ? ['show', '--name-only', '--pretty=%b', 'HEAD'] + : ['ls-files', '--other', '--modified', '--exclude-standard']; + const child = childProcess.spawn('git', args, {cwd}); + let stdout = ''; + let stderr = ''; + child.stdout.on('data', data => (stdout += data)); + child.stderr.on('data', data => (stderr += data)); + child.on('error', e => reject(e)); + child.on('close', code => { + if (code === 0) { + stdout = stdout.trim(); + if (stdout === '') { + resolve([]); + } else { + resolve( + stdout + .split('\n') + .map(changedPath => path.resolve(cwd, changedPath)), + ); + } } else { - resolve( - stdout - .split('\n') - .map(changedPath => path.resolve(cwd, changedPath)), - ); + reject(code + ': ' + stderr); } - } else { - reject(code + ': ' + stderr); - } + }); }); - }); -} - -function isGitRepository(cwd: string): Promise { - return new Promise(resolve => { - try { - let stdout = ''; - const options = ['rev-parse', '--show-toplevel']; - const child = childProcess.spawn('git', options, {cwd}); - child.stdout.on('data', data => (stdout += data)); - child.on('error', () => resolve(null)); - child.on('close', code => resolve(code === 0 ? stdout.trim() : null)); - } catch (e) { - resolve(null); - } - }); -} + }, -module.exports = { - findChangedFiles, - isGitRepository, + getRoot: async (cwd: string): Promise => { + return new Promise(resolve => { + try { + let stdout = ''; + const options = ['rev-parse', '--show-toplevel']; + const child = childProcess.spawn('git', options, {cwd}); + child.stdout.on('data', data => (stdout += data)); + child.on('error', () => resolve(null)); + child.on('close', code => resolve(code === 0 ? stdout.trim() : null)); + } catch (e) { + resolve(null); + } + }); + }, }; + +module.exports = adapter; diff --git a/packages/jest-changed-files/src/hg.js b/packages/jest-changed-files/src/hg.js index 3d92a22c8df9..a6d0eed96e73 100644 --- a/packages/jest-changed-files/src/hg.js +++ b/packages/jest-changed-files/src/hg.js @@ -9,6 +9,7 @@ */ import type {Path} from 'types/Config'; +import type {Options, SCMAdapter} from '../types'; import path from 'path'; import childProcess from 'child_process'; @@ -17,59 +18,56 @@ const env = Object.assign({}, process.env, { HGPLAIN: 1, }); -type Options = {| - lastCommit?: boolean, - withAncestor?: boolean, -|}; - -function findChangedFiles(cwd: string, options: Options): Promise> { - return new Promise((resolve, reject) => { - let args = ['status', '-amn']; - if (options && options.withAncestor) { - args.push('--rev', 'ancestor(.^)'); - } else if (options && options.lastCommit === true) { - args = ['tip', '--template', '{files%"{file}\n"}']; - } - const child = childProcess.spawn('hg', args, {cwd, env}); - let stdout = ''; - let stderr = ''; - child.stdout.on('data', data => (stdout += data)); - child.stderr.on('data', data => (stderr += data)); - child.on('error', e => reject(e)); - child.on('close', code => { - if (code === 0) { - stdout = stdout.trim(); - if (stdout === '') { - resolve([]); - } else { - resolve( - stdout - .split('\n') - .map(changedPath => path.resolve(cwd, changedPath)), - ); - } - } else { - reject(code + ': ' + stderr); +const adapter: SCMAdapter = { + findChangedFiles: async ( + cwd: string, + options: Options, + ): Promise> => { + return new Promise((resolve, reject) => { + let args = ['status', '-amnu']; + if (options && options.withAncestor) { + args.push('--rev', 'ancestor(.^)'); + } else if (options && options.lastCommit === true) { + args = ['tip', '--template', '{files%"{file}\n"}']; } - }); - }); -} - -function isHGRepository(cwd: string): Promise { - return new Promise(resolve => { - try { + const child = childProcess.spawn('hg', args, {cwd, env}); let stdout = ''; - const child = childProcess.spawn('hg', ['root'], {cwd, env}); + let stderr = ''; child.stdout.on('data', data => (stdout += data)); - child.on('error', () => resolve(null)); - child.on('close', code => resolve(code === 0 ? stdout.trim() : null)); - } catch (e) { - resolve(null); - } - }); -} + child.stderr.on('data', data => (stderr += data)); + child.on('error', e => reject(e)); + child.on('close', code => { + if (code === 0) { + stdout = stdout.trim(); + if (stdout === '') { + resolve([]); + } else { + resolve( + stdout + .split('\n') + .map(changedPath => path.resolve(cwd, changedPath)), + ); + } + } else { + reject(code + ': ' + stderr); + } + }); + }); + }, -module.exports = { - findChangedFiles, - isHGRepository, + getRoot: async (cwd: Path): Promise => { + return new Promise(resolve => { + try { + let stdout = ''; + const child = childProcess.spawn('hg', ['root'], {cwd, env}); + child.stdout.on('data', data => (stdout += data)); + child.on('error', () => resolve(null)); + child.on('close', code => resolve(code === 0 ? stdout.trim() : null)); + } catch (e) { + resolve(null); + } + }); + }, }; + +module.exports = adapter; diff --git a/packages/jest-changed-files/src/index.js b/packages/jest-changed-files/src/index.js index fb9197084ca0..a808f25c62a3 100644 --- a/packages/jest-changed-files/src/index.js +++ b/packages/jest-changed-files/src/index.js @@ -8,10 +8,61 @@ * @flow */ +import type {Path} from 'types/Config'; +import type {Options, Repos} from '../types'; + import git from './git'; import hg from './hg'; +import throat from 'throat'; + +// This is an arbitrary number. The main goal is to prevent projects with +// many roots (50+) from spawning too many processes at once. +const mutex = throat(5); + +const findGitRoot = dir => mutex(() => git.getRoot(dir)); +const findHgRoot = dir => mutex(() => hg.getRoot(dir)); + +const getChangedFilesForRoots = async ( + roots: Array, + options: Options, +): Promise<{changedFiles: Set, repos: Repos}> => { + const repos = await findRepos(roots); + const gitPromises = Array.from(repos.git).map(repo => + git.findChangedFiles(repo, options), + ); + + const hgPromises = Array.from(repos.hg).map(repo => + hg.findChangedFiles(repo, options), + ); + + const changedFiles = (await Promise.all( + gitPromises.concat(hgPromises), + )).reduce((allFiles, changedFilesInTheRepo) => { + for (const file of changedFilesInTheRepo) { + allFiles.add(file); + } + + return allFiles; + }, new Set()); + + return {changedFiles, repos}; +}; + +const findRepos = async (roots: Array): Promise => { + const gitRepos = await Promise.all( + roots.reduce((promises, root) => promises.concat(findGitRoot(root)), []), + ); + const hgRepos = await Promise.all( + roots.reduce((promises, root) => promises.concat(findHgRoot(root)), []), + ); + + return { + git: new Set(gitRepos.filter(Boolean)), + hg: new Set(hgRepos.filter(Boolean)), + }; +}; module.exports = { - git, - hg, + findRepos, + getChangedFilesForRoots, }; diff --git a/packages/jest-changed-files/types.js b/packages/jest-changed-files/types.js new file mode 100644 index 000000000000..c0dbb58d0e8d --- /dev/null +++ b/packages/jest-changed-files/types.js @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + */ + +import type {Path} from 'types/Config'; + +export type Options = {| + lastCommit?: boolean, + withAncestor?: boolean, +|}; + +export type Repos = {git: Set, hg: Set}; + +export type SCMAdapter = { + findChangedFiles: (cwd: Path, options: Options) => Promise>, + getRoot: (cwd: Path) => Promise, +}; diff --git a/packages/jest-changed-files/yarn.lock b/packages/jest-changed-files/yarn.lock new file mode 100644 index 000000000000..0cc1b1f9fb17 --- /dev/null +++ b/packages/jest-changed-files/yarn.lock @@ -0,0 +1,7 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +throat@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/throat/-/throat-4.0.0.tgz#e8d397aeb3f335c3bae404a83dc264b813a41e1b" diff --git a/packages/jest-cli/src/search_source.js b/packages/jest-cli/src/search_source.js index c71b2456e90b..281303fbd0bb 100644 --- a/packages/jest-cli/src/search_source.js +++ b/packages/jest-cli/src/search_source.js @@ -16,7 +16,7 @@ import type {Test} from 'types/TestRunner'; import path from 'path'; import micromatch from 'micromatch'; import DependencyResolver from 'jest-resolve-dependencies'; -import changedFiles from 'jest-changed-files'; +import {getChangedFilesForRoots} from 'jest-changed-files'; import {escapePathForRegex, replacePathSepForRegex} from 'jest-regex-util'; type SearchResult = {| @@ -44,11 +44,6 @@ export type TestSelectionConfig = {| watch?: boolean, |}; -const git = changedFiles.git; -const hg = changedFiles.hg; - -const determineSCM = path => - Promise.all([git.isGitRepository(path), hg.isHGRepository(path)]); const pathToRegex = p => replacePathSepForRegex(p); const globsToMatcher = (globs: ?Array) => { @@ -188,32 +183,18 @@ class SearchSource { return {tests: []}; } - findChangedTests(options: Options): Promise { - return Promise.all( - this._context.config.roots.map(determineSCM), - ).then(repos => { - if (!repos.every(([gitRepo, hgRepo]) => gitRepo || hgRepo)) { - return { - noSCM: true, - tests: [], - }; - } - return Promise.all( - repos.map(([gitRepo, hgRepo]) => { - if (gitRepo) { - return git.findChangedFiles(gitRepo, options); - } - if (hgRepo) { - return hg.findChangedFiles(hgRepo, options); - } - return []; - }), - ).then(changedPathSets => - this.findRelatedTests( - new Set(Array.prototype.concat.apply([], changedPathSets)), - ), - ); - }); + async findChangedTests(options: Options): Promise { + const {repos, changedFiles} = await getChangedFilesForRoots( + this._context.config.roots, + options, + ); + + // no SCM (git/hg/...) is found in any of the roots. + const noSCM = Object.keys(repos).every(scm => repos[scm].size === 0); + + return noSCM + ? {noSCM: true, tests: []} + : this.findRelatedTests(changedFiles); } getTestPaths(