Skip to content
61 changes: 61 additions & 0 deletions docs/config/experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,67 @@ export default defineConfig({
If you are running tests in Deno, TypeScript files are processed by the runtime without any additional configurations.
:::

## experimental.vcsProvider <Version type="experimental">4.1.1</Version> {#experimental-vcsprovider}

- **Type:** `VCSProvider | string`

```ts
interface VCSProvider {
findChangedFiles(options: VCSProviderOptions): Promise<string[]>
}

interface VCSProviderOptions {
root: string
changedSince?: string | boolean
}
```

- **Default:** `'git'`

Custom provider for detecting changed files. Used with the [`--changed`](/guide/cli#changed) flag to determine which files have been modified.

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:

```ts [vitest.config.ts]
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
experimental: {
vcsProvider: {
async findChangedFiles({ root, changedSince }) {
// return paths of changed files
return []
},
},
},
},
})
```

You can also pass a string path to a module with a default export that implements the `VCSProvider` interface:

```js [vitest.config.js]
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
experimental: {
vcsProvider: './my-vcs-provider.js',
},
},
})
```

```js [my-vcs-provider.js]
export default {
async findChangedFiles({ root, changedSince }) {
// return paths of changed files
return []
},
}
```

## experimental.nodeLoader <Version type="experimental">4.1.0</Version> {#experimental-nodeloader}

- **Type:** `boolean`
Expand Down
7 changes: 7 additions & 0 deletions docs/guide/cli-generated.md
Original file line number Diff line number Diff line change
Expand Up @@ -949,3 +949,10 @@ Control whether Vitest uses Vite's module runner to run the code or fallback to
- **Config:** [experimental.nodeLoader](/config/experimental#experimental-nodeloader)

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`)

### experimental.vcsProvider

- **CLI:** `--experimental.vcsProvider <path>`
- **Config:** [experimental.vcsProvider](/config/experimental#experimental-vcsprovider)

Custom provider for detecting changed files. (default: `git`)
5 changes: 5 additions & 0 deletions packages/vitest/src/node/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,11 @@ export const cliOptionsConfig: VitestCLIOptions = {
nodeLoader: {
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`)',
},
vcsProvider: {
argument: '<path>',
description: 'Custom provider for detecting changed files. (default: `git`)',
subcommands: null,
},
},
},
// disable CLI options
Expand Down
4 changes: 4 additions & 0 deletions packages/vitest/src/node/config/resolveConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,10 @@ export function resolveConfig(
resolved.experimental.importDurations.thresholds.warn ??= 100
resolved.experimental.importDurations.thresholds.danger ??= 500

if (typeof resolved.experimental.vcsProvider === 'string' && resolved.experimental.vcsProvider !== 'git') {
resolved.experimental.vcsProvider = resolvePath(resolved.experimental.vcsProvider, resolved.root)
}

return resolved
}

Expand Down
10 changes: 10 additions & 0 deletions packages/vitest/src/node/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { ResolvedConfig, TestProjectConfiguration, UserConfig, VitestRunMod
import type { CoverageProvider, ResolvedCoverageOptions } from './types/coverage'
import type { Reporter } from './types/reporter'
import type { TestRunResult } from './types/tests'
import type { VCSProvider } from './vcs/vcs'
import os, { tmpdir } from 'node:os'
import { getTasks, hasFailed, limitConcurrency } from '@vitest/runner/utils'
import { SnapshotManager } from '@vitest/snapshot/manager'
Expand Down Expand Up @@ -51,6 +52,7 @@ import { VitestSpecifications } from './specifications'
import { StateManager } from './state'
import { populateProjectsTags } from './tags'
import { TestRun } from './test-run'
import { loadVCSProvider } from './vcs/vcs'
import { VitestWatcher } from './watcher'

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

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

if (this.config.watch) {
// hijack server restart
Expand Down
14 changes: 10 additions & 4 deletions packages/vitest/src/node/coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,11 +331,17 @@ export class BaseCoverageProvider {

async onTestRunStart(): Promise<void> {
if (this.options.changed) {
const { VitestGit } = await import('./git')
const vitestGit = new VitestGit(this.ctx.config.root)
const changedFiles = await vitestGit.findChangedFiles({ changedSince: this.options.changed })
try {
const changedFiles = await this.ctx.vcs.findChangedFiles({
root: this.ctx.config.root,
changedSince: this.options.changed,
})

this.changedFiles = changedFiles ?? undefined
this.changedFiles = changedFiles
}
catch {
this.changedFiles = undefined
}
}
else if (this.ctx.config.changed) {
this.changedFiles = this.ctx.config.related
Expand Down
11 changes: 3 additions & 8 deletions packages/vitest/src/node/specifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { join, relative, resolve } from 'pathe'
import pm from 'picomatch'
import { isWindows } from '../utils/env'
import { groupFilters, parseFilter } from './cli/filter'
import { GitNotFoundError, IncludeTaskLocationDisabledError, LocationFilterFileNotFoundError } from './errors'
import { IncludeTaskLocationDisabledError, LocationFilterFileNotFoundError } from './errors'

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

private async filterTestsBySource(specs: TestSpecification[]): Promise<TestSpecification[]> {
if (this.vitest.config.changed && !this.vitest.config.related) {
const { VitestGit } = await import('./git')
const vitestGit = new VitestGit(this.vitest.config.root)
const related = await vitestGit.findChangedFiles({
const related = await this.vitest.vcs.findChangedFiles({
root: this.vitest.config.root,
changedSince: this.vitest.config.changed,
})
if (!related) {
process.exitCode = 1
throw new GitNotFoundError()
}
this.vitest.config.related = Array.from(new Set(related))
}

Expand Down
10 changes: 10 additions & 0 deletions packages/vitest/src/node/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
} from '../reporters'
import type { TestCase, TestModule, TestSuite } from '../reporters/reported-tasks'
import type { TestSequencerConstructor } from '../sequencers/types'
import type { VCSProvider } from '../vcs/vcs'
import type { WatcherTriggerPattern } from '../watcher'
import type { BenchmarkUserOptions } from './benchmark'
import type { BrowserConfigOptions, ResolvedBrowserOptions } from './browser'
Expand Down Expand Up @@ -937,6 +938,15 @@ export interface InlineConfig {
* This option only affects `loader.load` method, Vitest always defines a `loader.resolve` to populate the module graph.
*/
nodeLoader?: boolean

/**
* Custom provider for detecting changed files. Used with the `--changed` flag
* to determine which files have been modified.
*
* 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.
*/
vcsProvider?: VCSProvider | string
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import type { Output } from 'tinyexec'
import type { VCSProvider, VCSProviderOptions } from './vcs'
import { resolve } from 'pathe'
import { x } from 'tinyexec'
import { GitNotFoundError } from '../errors'

export interface GitOptions {
changedSince?: string | boolean
}

export class VitestGit {
export class GitVCSProvider implements VCSProvider {
private root!: string

constructor(private cwd: string) {}

private async resolveFilesWithGitCommand(args: string[]): Promise<string[]> {
let result: Output

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

async findChangedFiles(options: GitOptions): Promise<string[] | null> {
const root = await this.getRoot(this.cwd)
async findChangedFiles(options: VCSProviderOptions): Promise<string[]> {
const root = this.root || await this.getRoot(options.root)
if (!root) {
return null
throw new GitNotFoundError()
}

this.root = root
Expand Down
36 changes: 36 additions & 0 deletions packages/vitest/src/node/vcs/vcs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { ModuleRunner } from 'vite/module-runner'
import { resolve } from 'pathe'
import { GitVCSProvider } from './git'

export interface VCSProviderOptions {
root: string
changedSince?: string | boolean
}

export interface VCSProvider {
// eslint-disable-next-line ts/method-signature-style
findChangedFiles(options: VCSProviderOptions): Promise<string[]>
}

export async function loadVCSProvider(runner: ModuleRunner, vcsProvider: string | VCSProvider | undefined): Promise<VCSProvider> {
if (typeof vcsProvider === 'object' && vcsProvider != null) {
return wrapVCSProvider(vcsProvider)
}
if (!vcsProvider || vcsProvider === 'git') {
return new GitVCSProvider()
}
const module = await runner.import(vcsProvider) as { default: VCSProvider }
if (!module.default || typeof module.default !== 'object' || typeof module.default.findChangedFiles !== 'function') {
throw new Error(`The vcsProvider module '${vcsProvider}' doesn't have a default export with \`findChangedFiles\` method.`)
}
return wrapVCSProvider(module.default)
}

function wrapVCSProvider(provider: VCSProvider): VCSProvider {
return {
async findChangedFiles(options) {
const changedFiles = await provider.findChangedFiles(options)
return changedFiles.map(file => resolve(options.root, file))
},
}
}
Loading
Loading