Skip to content

Commit 5611500

Browse files
authored
feat(experimental): introduce experimental.vcsProvider (#9928)
1 parent eec53d9 commit 5611500

File tree

11 files changed

+347
-22
lines changed

11 files changed

+347
-22
lines changed

docs/config/experimental.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,67 @@ export default defineConfig({
410410
If you are running tests in Deno, TypeScript files are processed by the runtime without any additional configurations.
411411
:::
412412

413+
## experimental.vcsProvider <Version type="experimental">4.1.1</Version> {#experimental-vcsprovider}
414+
415+
- **Type:** `VCSProvider | string`
416+
417+
```ts
418+
interface VCSProvider {
419+
findChangedFiles(options: VCSProviderOptions): Promise<string[]>
420+
}
421+
422+
interface VCSProviderOptions {
423+
root: string
424+
changedSince?: string | boolean
425+
}
426+
```
427+
428+
- **Default:** `'git'`
429+
430+
Custom provider for detecting changed files. Used with the [`--changed`](/guide/cli#changed) flag to determine which files have been modified.
431+
432+
By default, Vitest uses Git to detect changed files. You can provide a custom implementation of the `VCSProvider` interface to use a different version control system:
433+
434+
```ts [vitest.config.ts]
435+
import { defineConfig } from 'vitest/config'
436+
437+
export default defineConfig({
438+
test: {
439+
experimental: {
440+
vcsProvider: {
441+
async findChangedFiles({ root, changedSince }) {
442+
// return paths of changed files
443+
return []
444+
},
445+
},
446+
},
447+
},
448+
})
449+
```
450+
451+
You can also pass a string path to a module with a default export that implements the `VCSProvider` interface:
452+
453+
```js [vitest.config.js]
454+
import { defineConfig } from 'vitest/config'
455+
456+
export default defineConfig({
457+
test: {
458+
experimental: {
459+
vcsProvider: './my-vcs-provider.js',
460+
},
461+
},
462+
})
463+
```
464+
465+
```js [my-vcs-provider.js]
466+
export default {
467+
async findChangedFiles({ root, changedSince }) {
468+
// return paths of changed files
469+
return []
470+
},
471+
}
472+
```
473+
413474
## experimental.nodeLoader <Version type="experimental">4.1.0</Version> {#experimental-nodeloader}
414475

415476
- **Type:** `boolean`

docs/guide/cli-generated.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,3 +949,10 @@ Control whether Vitest uses Vite's module runner to run the code or fallback to
949949
- **Config:** [experimental.nodeLoader](/config/experimental#experimental-nodeloader)
950950

951951
Controls whether Vitest will use Node.js Loader API to process in-source or mocked files. This has no effect if `viteModuleRunner` is enabled. Disabling this can increase performance. (default: `true`)
952+
953+
### experimental.vcsProvider
954+
955+
- **CLI:** `--experimental.vcsProvider <path>`
956+
- **Config:** [experimental.vcsProvider](/config/experimental#experimental-vcsprovider)
957+
958+
Custom provider for detecting changed files. (default: `git`)

packages/vitest/src/node/cli/cli-config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -901,6 +901,11 @@ export const cliOptionsConfig: VitestCLIOptions = {
901901
nodeLoader: {
902902
description: 'Controls whether Vitest will use Node.js Loader API to process in-source or mocked files. This has no effect if `viteModuleRunner` is enabled. Disabling this can increase performance. (default: `true`)',
903903
},
904+
vcsProvider: {
905+
argument: '<path>',
906+
description: 'Custom provider for detecting changed files. (default: `git`)',
907+
subcommands: null,
908+
},
904909
},
905910
},
906911
// disable CLI options

packages/vitest/src/node/config/resolveConfig.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,10 @@ export function resolveConfig(
950950
resolved.experimental.importDurations.thresholds.warn ??= 100
951951
resolved.experimental.importDurations.thresholds.danger ??= 500
952952

953+
if (typeof resolved.experimental.vcsProvider === 'string' && resolved.experimental.vcsProvider !== 'git') {
954+
resolved.experimental.vcsProvider = resolvePath(resolved.experimental.vcsProvider, resolved.root)
955+
}
956+
953957
return resolved
954958
}
955959

packages/vitest/src/node/core.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { ResolvedConfig, TestProjectConfiguration, UserConfig, VitestRunMod
1515
import type { CoverageProvider, ResolvedCoverageOptions } from './types/coverage'
1616
import type { Reporter } from './types/reporter'
1717
import type { TestRunResult } from './types/tests'
18+
import type { VCSProvider } from './vcs/vcs'
1819
import os, { tmpdir } from 'node:os'
1920
import { getTasks, hasFailed, limitConcurrency } from '@vitest/runner/utils'
2021
import { SnapshotManager } from '@vitest/snapshot/manager'
@@ -51,6 +52,7 @@ import { VitestSpecifications } from './specifications'
5152
import { StateManager } from './state'
5253
import { populateProjectsTags } from './tags'
5354
import { TestRun } from './test-run'
55+
import { loadVCSProvider } from './vcs/vcs'
5456
import { VitestWatcher } from './watcher'
5557

5658
const WATCHER_DEBOUNCE = 100
@@ -101,6 +103,13 @@ export class Vitest {
101103
* Vitest behaviour.
102104
*/
103105
public readonly watcher: VitestWatcher
106+
/**
107+
* The version control system provider used to detect changed files.
108+
* This is used with the `--changed` flag to determine which test files to run.
109+
* By default, Vitest uses Git. You can provide a custom implementation via
110+
* `experimental.vcsProvider` in your config.
111+
*/
112+
public vcs!: VCSProvider
104113

105114
/** @internal */ configOverride: Partial<ResolvedConfig> = {}
106115
/** @internal */ filenamePattern?: string[]
@@ -263,6 +272,7 @@ export class Vitest {
263272
configurable: true,
264273
})
265274
}
275+
this.vcs = await loadVCSProvider(this.runner, resolved.experimental.vcsProvider)
266276

267277
if (this.config.watch) {
268278
// hijack server restart

packages/vitest/src/node/coverage.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -331,11 +331,17 @@ export class BaseCoverageProvider {
331331

332332
async onTestRunStart(): Promise<void> {
333333
if (this.options.changed) {
334-
const { VitestGit } = await import('./git')
335-
const vitestGit = new VitestGit(this.ctx.config.root)
336-
const changedFiles = await vitestGit.findChangedFiles({ changedSince: this.options.changed })
334+
try {
335+
const changedFiles = await this.ctx.vcs.findChangedFiles({
336+
root: this.ctx.config.root,
337+
changedSince: this.options.changed,
338+
})
337339

338-
this.changedFiles = changedFiles ?? undefined
340+
this.changedFiles = changedFiles
341+
}
342+
catch {
343+
this.changedFiles = undefined
344+
}
339345
}
340346
else if (this.ctx.config.changed) {
341347
this.changedFiles = this.ctx.config.related

packages/vitest/src/node/specifications.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { join, relative, resolve } from 'pathe'
66
import pm from 'picomatch'
77
import { isWindows } from '../utils/env'
88
import { groupFilters, parseFilter } from './cli/filter'
9-
import { GitNotFoundError, IncludeTaskLocationDisabledError, LocationFilterFileNotFoundError } from './errors'
9+
import { IncludeTaskLocationDisabledError, LocationFilterFileNotFoundError } from './errors'
1010

1111
export class VitestSpecifications {
1212
private readonly _cachedSpecs = new Map<string, TestSpecification[]>()
@@ -121,15 +121,10 @@ export class VitestSpecifications {
121121

122122
private async filterTestsBySource(specs: TestSpecification[]): Promise<TestSpecification[]> {
123123
if (this.vitest.config.changed && !this.vitest.config.related) {
124-
const { VitestGit } = await import('./git')
125-
const vitestGit = new VitestGit(this.vitest.config.root)
126-
const related = await vitestGit.findChangedFiles({
124+
const related = await this.vitest.vcs.findChangedFiles({
125+
root: this.vitest.config.root,
127126
changedSince: this.vitest.config.changed,
128127
})
129-
if (!related) {
130-
process.exitCode = 1
131-
throw new GitNotFoundError()
132-
}
133128
this.vitest.config.related = Array.from(new Set(related))
134129
}
135130

packages/vitest/src/node/types/config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
} from '../reporters'
1818
import type { TestCase, TestModule, TestSuite } from '../reporters/reported-tasks'
1919
import type { TestSequencerConstructor } from '../sequencers/types'
20+
import type { VCSProvider } from '../vcs/vcs'
2021
import type { WatcherTriggerPattern } from '../watcher'
2122
import type { BenchmarkUserOptions } from './benchmark'
2223
import type { BrowserConfigOptions, ResolvedBrowserOptions } from './browser'
@@ -937,6 +938,15 @@ export interface InlineConfig {
937938
* This option only affects `loader.load` method, Vitest always defines a `loader.resolve` to populate the module graph.
938939
*/
939940
nodeLoader?: boolean
941+
942+
/**
943+
* Custom provider for detecting changed files. Used with the `--changed` flag
944+
* to determine which files have been modified.
945+
*
946+
* By default, Vitest uses Git to detect changed files. You can provide a custom
947+
* implementation of the `VCSProvider` interface to use a different version control system.
948+
*/
949+
vcsProvider?: VCSProvider | string
940950
}
941951

942952
/**
Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
import type { Output } from 'tinyexec'
2+
import type { VCSProvider, VCSProviderOptions } from './vcs'
23
import { resolve } from 'pathe'
34
import { x } from 'tinyexec'
5+
import { GitNotFoundError } from '../errors'
46

5-
export interface GitOptions {
6-
changedSince?: string | boolean
7-
}
8-
9-
export class VitestGit {
7+
export class GitVCSProvider implements VCSProvider {
108
private root!: string
119

12-
constructor(private cwd: string) {}
13-
1410
private async resolveFilesWithGitCommand(args: string[]): Promise<string[]> {
1511
let result: Output
1612

@@ -29,10 +25,10 @@ export class VitestGit {
2925
.map(changedPath => resolve(this.root, changedPath))
3026
}
3127

32-
async findChangedFiles(options: GitOptions): Promise<string[] | null> {
33-
const root = await this.getRoot(this.cwd)
28+
async findChangedFiles(options: VCSProviderOptions): Promise<string[]> {
29+
const root = this.root || await this.getRoot(options.root)
3430
if (!root) {
35-
return null
31+
throw new GitNotFoundError()
3632
}
3733

3834
this.root = root
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { ModuleRunner } from 'vite/module-runner'
2+
import { resolve } from 'pathe'
3+
import { GitVCSProvider } from './git'
4+
5+
export interface VCSProviderOptions {
6+
root: string
7+
changedSince?: string | boolean
8+
}
9+
10+
export interface VCSProvider {
11+
// eslint-disable-next-line ts/method-signature-style
12+
findChangedFiles(options: VCSProviderOptions): Promise<string[]>
13+
}
14+
15+
export async function loadVCSProvider(runner: ModuleRunner, vcsProvider: string | VCSProvider | undefined): Promise<VCSProvider> {
16+
if (typeof vcsProvider === 'object' && vcsProvider != null) {
17+
return wrapVCSProvider(vcsProvider)
18+
}
19+
if (!vcsProvider || vcsProvider === 'git') {
20+
return new GitVCSProvider()
21+
}
22+
const module = await runner.import(vcsProvider) as { default: VCSProvider }
23+
if (!module.default || typeof module.default !== 'object' || typeof module.default.findChangedFiles !== 'function') {
24+
throw new Error(`The vcsProvider module '${vcsProvider}' doesn't have a default export with \`findChangedFiles\` method.`)
25+
}
26+
return wrapVCSProvider(module.default)
27+
}
28+
29+
function wrapVCSProvider(provider: VCSProvider): VCSProvider {
30+
return {
31+
async findChangedFiles(options) {
32+
const changedFiles = await provider.findChangedFiles(options)
33+
return changedFiles.map(file => resolve(options.root, file))
34+
},
35+
}
36+
}

0 commit comments

Comments
 (0)