Skip to content

Commit

Permalink
feat: changed-files-ignore-pattern option (#3797)
Browse files Browse the repository at this point in the history
  • Loading branch information
ibezkrovnyi authored Sep 26, 2021
1 parent ea69daa commit fe5688d
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 5 deletions.
8 changes: 8 additions & 0 deletions .changeset/pretty-hornets-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@pnpm/common-cli-options-help": minor
"@pnpm/config": minor
"@pnpm/filter-workspace-packages": minor
"pnpm": minor
---

Add option 'changed-files-ignore-pattern' to ignore changed files by glob patterns when filtering for changed projects since the specified commit/branch.
4 changes: 4 additions & 0 deletions packages/common-cli-options-help/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ export const FILTERING = {
description: 'Defines files related to tests. Useful with the changed since filter. When selecting only changed packages and their dependent packages, the dependent packages will be ignored in case a package has changes only in tests. Usage example: pnpm --filter="...[origin/master]" --test-pattern="test/*" test',
name: '--test-pattern <pattern>',
},
{
description: 'Defines files to ignore when filtering for changed projects since the specified commit/branch. Usage example: pnpm --filter="...[origin/master]" --changed-files-ignore-pattern="**/README.md" build',
name: '--changed-files-ignore-pattern <pattern>',
},
{
description: 'Restricts the scope to package names matching the given pattern similar to --filter, but it ignores devDependencies when searching for dependencies and dependents.',
name: '--filter-prod <pattern>',
Expand Down
1 change: 1 addition & 0 deletions packages/config/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export interface Config {
workspaceRoot: boolean

testPattern?: string[]
changedFilesIgnorePattern?: string[]
}

export interface ConfigWithDeprecatedSettings extends Config {
Expand Down
1 change: 1 addition & 0 deletions packages/config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export const types = Object.assign({
'workspace-packages': [String, Array],
'workspace-root': Boolean,
'test-pattern': [String, Array],
'changed-files-ignore-pattern': [String, Array],
}, npmTypes.types)

export type CliOptions = Record<string, unknown> & { dir?: string }
Expand Down
36 changes: 36 additions & 0 deletions packages/config/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,42 @@ test('respects test-pattern', async () => {
}
})

test('respects changed-files-ignore-pattern', async () => {
{
const { config } = await getConfig({
cliOptions: {},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})

expect(config.changedFilesIgnorePattern).toBeUndefined()
}
{
prepareEmpty()

const npmrc = [
'changed-files-ignore-pattern[]=.github/**',
'changed-files-ignore-pattern[]=**/README.md',
].join('\n')

await fs.writeFile('.npmrc', npmrc, 'utf8')

const { config } = await getConfig({
cliOptions: {
global: false,
},
packageManager: {
name: 'pnpm',
version: '1.0.0',
},
})

expect(config.changedFilesIgnorePattern).toEqual(['.github/**', '**/README.md'])
}
})

test('dir is resolved to real path', async () => {
prepareEmpty()
const realDir = path.resolve('real-path')
Expand Down
16 changes: 12 additions & 4 deletions packages/filter-workspace-packages/src/getChangedPackages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ type ChangeType = 'source' | 'test'

interface ChangedDir { dir: string, changeType: ChangeType }

export default async function changedSince (packageDirs: string[], commit: string, opts: { workspaceDir: string, testPattern?: string[] }): Promise<[string[], string[]]> {
export default async function changedSince (packageDirs: string[], commit: string, opts: { workspaceDir: string, testPattern?: string[], changedFilesIgnorePattern?: string[] }): Promise<[string[], string[]]> {
const repoRoot = path.resolve(await findUp('.git', { cwd: opts.workspaceDir, type: 'directory' }) ?? opts.workspaceDir, '..')
const changedDirs = (await getChangedDirsSinceCommit(commit, opts.workspaceDir, opts.testPattern ?? []))
const changedDirs = (await getChangedDirsSinceCommit(commit, opts.workspaceDir, opts.testPattern ?? [], opts.changedFilesIgnorePattern ?? []))
.map(changedDir => ({ ...changedDir, dir: path.join(repoRoot, changedDir.dir) }))
const pkgChangeTypes = new Map<string, ChangeType | undefined>()
for (const pkgDir of packageDirs) {
Expand Down Expand Up @@ -42,7 +42,7 @@ export default async function changedSince (packageDirs: string[], commit: strin
return [changedPkgs, ignoreDependentForPkgs]
}

async function getChangedDirsSinceCommit (commit: string, workingDir: string, testPattern: string[]): Promise<ChangedDir[]> {
async function getChangedDirsSinceCommit (commit: string, workingDir: string, testPattern: string[], changedFilesIgnorePattern: string[]): Promise<ChangedDir[]> {
let diff!: string
try {
diff = (
Expand All @@ -63,7 +63,15 @@ async function getChangedDirsSinceCommit (commit: string, workingDir: string, te
return []
}

const changedFiles = diff.split('\n')
const allChangedFiles = diff.split('\n')
const patterns = changedFilesIgnorePattern.filter(
(pattern) => pattern.length
)
const changedFiles = patterns.length
? micromatch.not(allChangedFiles, patterns, {
dot: true,
})
: allChangedFiles

for (const changedFile of changedFiles) {
const dir = path.dirname(changedFile)
Expand Down
10 changes: 9 additions & 1 deletion packages/filter-workspace-packages/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export async function readProjects (
pkgSelectors: PackageSelector[],
opts?: {
linkWorkspacePackages?: boolean
changedFilesIgnorePattern?: string[]
}
) {
const allProjects = await findWorkspacePackages(workspaceDir, {})
Expand All @@ -43,6 +44,7 @@ export async function readProjects (
{
linkWorkspacePackages: opts?.linkWorkspacePackages,
workspaceDir,
changedFilesIgnorePattern: opts?.changedFilesIgnorePattern,
}
)
return { allProjects, selectedProjectsGraph }
Expand All @@ -56,6 +58,7 @@ export async function filterPackages<T> (
prefix: string
workspaceDir: string
testPattern?: string[]
changedFilesIgnorePattern?: string[]
useGlobDirFiltering?: boolean
}
): Promise<{
Expand All @@ -74,6 +77,7 @@ export async function filterPkgsBySelectorObjects<T> (
linkWorkspacePackages?: boolean
workspaceDir: string
testPattern?: string[]
changedFilesIgnorePattern?: string[]
useGlobDirFiltering?: boolean
}
): Promise<{
Expand All @@ -90,6 +94,7 @@ export async function filterPkgsBySelectorObjects<T> (
filteredGraph = await filterGraph(graph, allPackageSelectors, {
workspaceDir: opts.workspaceDir,
testPattern: opts.testPattern,
changedFilesIgnorePattern: opts.changedFilesIgnorePattern,
useGlobDirFiltering: opts.useGlobDirFiltering,
})
}
Expand All @@ -101,6 +106,7 @@ export async function filterPkgsBySelectorObjects<T> (
prodFilteredGraph = await filterGraph(graph, prodPackageSelectors, {
workspaceDir: opts.workspaceDir,
testPattern: opts.testPattern,
changedFilesIgnorePattern: opts.changedFilesIgnorePattern,
useGlobDirFiltering: opts.useGlobDirFiltering,
})
}
Expand All @@ -127,6 +133,7 @@ export default async function filterGraph<T> (
opts: {
workspaceDir: string
testPattern?: string[]
changedFilesIgnorePattern?: string[]
useGlobDirFiltering?: boolean
}
): Promise<{
Expand Down Expand Up @@ -156,6 +163,7 @@ async function _filterGraph<T> (
opts: {
workspaceDir: string
testPattern?: string[]
changedFilesIgnorePattern?: string[]
useGlobDirFiltering?: boolean
},
packageSelectors: PackageSelector[]
Expand All @@ -178,7 +186,7 @@ async function _filterGraph<T> (
if (selector.diff) {
let ignoreDependentForPkgs: string[] = []
;[entryPackages, ignoreDependentForPkgs] = await getChangedPkgs(Object.keys(pkgGraph),
selector.diff, { workspaceDir: selector.parentDir ?? opts.workspaceDir, testPattern: opts.testPattern })
selector.diff, { workspaceDir: selector.parentDir ?? opts.workspaceDir, testPattern: opts.testPattern, changedFilesIgnorePattern: opts.changedFilesIgnorePattern })
selectEntries({
...selector,
includeDependents: false,
Expand Down
1 change: 1 addition & 0 deletions packages/pnpm/src/cmd/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const GLOBAL_OPTIONS = pick([
'reporter',
'stream',
'test-pattern',
'changed-files-ignore-pattern',
'use-stderr',
'workspace-packages',
'workspace-root',
Expand Down
1 change: 1 addition & 0 deletions packages/pnpm/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export default async function run (inputArgv: string[]) {
prefix: process.cwd(),
workspaceDir: wsDir,
testPattern: config.testPattern,
changedFilesIgnorePattern: config.changedFilesIgnorePattern,
useGlobDirFiltering: config.useBetaCli,
})
config.selectedProjectsGraph = filterResults.selectedProjectsGraph
Expand Down
105 changes: 105 additions & 0 deletions packages/pnpm/test/monorepo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,111 @@ test('test-pattern is respected by the test script', async () => {
expect(output.sort()).toStrictEqual(['project-2', 'project-4'])
})

test('changed-files-ignore-pattern is respected', async () => {
const remote = tempy.directory()

preparePackages([
{
name: 'project-1-no-changes',
version: '1.0.0',
},
{
name: 'project-2-change-is-never-ignored',
version: '1.0.0',
},
{
name: 'project-3-ignored-by-pattern',
version: '1.0.0',
},
{
name: 'project-4-ignored-by-pattern',
version: '1.0.0',
},
{
name: 'project-5-ignored-by-pattern',
version: '1.0.0',
},
])

await execa('git', ['init'])
await execa('git', ['config', 'user.email', '[email protected]'])
await execa('git', ['config', 'user.name', 'xyz'])
await execa('git', ['init', '--bare'], { cwd: remote })
await execa('git', ['add', '*'])
await execa('git', ['commit', '-m', 'init', '--no-gpg-sign'])
await execa('git', ['remote', 'add', 'origin', remote])
await execa('git', ['push', '-u', 'origin', 'master'])

const npmrcLines = []
await fs.writeFile('project-2-change-is-never-ignored/index.js', '')

npmrcLines.push('changed-files-ignore-pattern[]=**/{*.spec.js,*.md}')
await fs.writeFile('project-3-ignored-by-pattern/index.spec.js', '')
await fs.writeFile('project-3-ignored-by-pattern/README.md', '')

npmrcLines.push('changed-files-ignore-pattern[]=**/buildscript.js')
await fs.mkdir('project-4-ignored-by-pattern/a/b/c', {
recursive: true,
})
await fs.writeFile('project-4-ignored-by-pattern/a/b/c/buildscript.js', '')

npmrcLines.push('changed-files-ignore-pattern[]=**/cache/**')
await fs.mkdir('project-5-ignored-by-pattern/cache/a/b', {
recursive: true,
})
await fs.writeFile('project-5-ignored-by-pattern/cache/a/b/index.js', '')

await writeYamlFile('pnpm-workspace.yaml', { packages: ['**', '!store/**'] })

await execa('git', ['add', '.'])
await execa('git', [
'commit',
'--allow-empty-message',
'-m',
'',
'--no-gpg-sign',
])

await fs.writeFile('.npmrc', npmrcLines.join('\n'), 'utf8')
await execPnpm(['install'])

const getChangedProjects = async (opts?: {
overrideChangedFilesIgnorePatternWithNoPattern: boolean
}) => {
const result = await execPnpmSync(
[
'--filter',
'[origin/master]',
opts?.overrideChangedFilesIgnorePatternWithNoPattern
? '--changed-files-ignore-pattern='
: '',
'ls',
'--depth',
'-1',
'--json',
].filter(Boolean)
)
return JSON.parse(result.stdout.toString())
.map((p: { name: string }) => p.name)
.sort()
}

expect(await getChangedProjects()).toStrictEqual([
'project-2-change-is-never-ignored',
])

expect(
await getChangedProjects({
overrideChangedFilesIgnorePatternWithNoPattern: true,
})
).toStrictEqual([
'project-2-change-is-never-ignored',
'project-3-ignored-by-pattern',
'project-4-ignored-by-pattern',
'project-5-ignored-by-pattern',
])
})

test('do not get confused by filtered dependencies when searching for dependents in monorepo', async () => {
/*
In this test case, we are filtering for 'project-2' and its dependents with
Expand Down

0 comments on commit fe5688d

Please sign in to comment.