Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(git): fix exclude files handling in subtree Git repo scan mode #5504

Merged
merged 15 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 46 additions & 16 deletions core/src/vcs/git-repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,51 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { GitHandler, augmentGlobs } from "./git.js"
import type { GetFilesParams, VcsFile } from "./vcs.js"
import { augmentGlobs, GitHandler } from "./git.js"
import type { BaseIncludeExcludeFiles, GetFilesParams, IncludeExcludeFilesHandler, VcsFile } from "./vcs.js"
import { isDirectory, matchPath } from "../util/fs.js"
import fsExtra from "fs-extra"
const { pathExists } = fsExtra
import { pathToCacheContext } from "../cache.js"
import { FileTree } from "./file-tree.js"
import { sep } from "path"
import { normalize, sep } from "path"

const { pathExists } = fsExtra

type ScanRepoParams = Pick<GetFilesParams, "log" | "path" | "pathDescription" | "failOnPrompt">

interface GitRepoGetFilesParams extends GetFilesParams {
scanFromProjectRoot: boolean
}

interface GitRepoIncludeExcludeFiles extends BaseIncludeExcludeFiles {
augmentedIncludes: string[]
augmentedExcludes: string[]
}

const getIncludeExcludeFiles: IncludeExcludeFilesHandler<GitRepoGetFilesParams, GitRepoIncludeExcludeFiles> = async (
params
) => {
const { include, path, scanFromProjectRoot } = params

// Make sure action config is not mutated.
let exclude = !params.exclude ? [] : [...params.exclude]

// Do the same normalization of the excluded paths like in `GitHandler`.
// This might be redundant because the non-normalized paths will be handled by `augmentGlobs` below.
// But this brings no harm and makes the implementation more clear.
exclude = exclude.map(normalize)

// We allow just passing a path like `foo` as include and exclude params
// Those need to be converted to globs, but we don't want to touch existing globs
const augmentedIncludes = include ? await augmentGlobs(path, include) : ["**/*"]
const augmentedExcludes = await augmentGlobs(path, exclude || [])
if (scanFromProjectRoot) {
augmentedExcludes.push("**/.garden/**/*")
}

return { include, exclude, augmentedIncludes, augmentedExcludes }
}

export class GitRepoHandler extends GitHandler {
override name = "git-repo"

Expand Down Expand Up @@ -58,20 +92,16 @@ export class GitRepoHandler extends GitHandler {

const moduleFiles = fileTree.getFilesAtPath(path)

// We allow just passing a path like `foo` as include and exclude params
// Those need to be converted to globs, but we don't want to touch existing globs
const include = params.include ? await augmentGlobs(path, params.include) : ["**/*"]
const exclude = await augmentGlobs(path, params.exclude || [])

if (scanRoot === this.garden?.projectRoot) {
exclude.push("**/.garden/**/*")
}
const scanFromProjectRoot = scanRoot === this.garden?.projectRoot
const { augmentedExcludes, augmentedIncludes } = await getIncludeExcludeFiles({ ...params, scanFromProjectRoot })

log.debug(
`Found ${moduleFiles.length} files in module path, filtering by ${include.length} include and ${exclude.length} exclude globs`
`Found ${moduleFiles.length} files in module path, filtering by ${augmentedIncludes.length} include and ${augmentedExcludes.length} exclude globs`
)
log.silly(() => `Include globs: ${augmentedIncludes.join(", ")}`)
log.silly(() =>
augmentedExcludes.length > 0 ? `Exclude globs: ${augmentedExcludes.join(", ")}` : "No exclude globs"
)
log.silly(() => `Include globs: ${include.join(", ")}`)
log.silly(() => (exclude.length > 0 ? `Exclude globs: ${exclude.join(", ")}` : "No exclude globs"))

const filtered = moduleFiles.filter(({ path: p }) => {
if (filter && !filter(p)) {
Expand All @@ -83,7 +113,7 @@ export class GitRepoHandler extends GitHandler {
// but that caused issues with the glob matching on windows due to backslashes
const relativePath = p.replace(`${path}${sep}`, "")
log.silly(() => `Checking if ${relativePath} matches include/exclude globs`)
return matchPath(relativePath, include, exclude)
return matchPath(relativePath, augmentedIncludes, augmentedExcludes)
})

log.debug(`Found ${filtered.length} files in module path after glob matching`)
Expand Down
83 changes: 53 additions & 30 deletions core/src/vcs/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@
*/

import { performance } from "perf_hooks"
import { isAbsolute, join, posix, relative, resolve } from "path"
import { isAbsolute, join, normalize, posix, relative, resolve } from "path"
import { isString } from "lodash-es"
import fsExtra from "fs-extra"
import { PassThrough } from "stream"
import type { GetFilesParams, RemoteSourceParams, VcsFile, VcsHandlerParams, VcsInfo } from "./vcs.js"
import type {
BaseIncludeExcludeFiles,
GetFilesParams,
IncludeExcludeFilesHandler,
RemoteSourceParams,
VcsFile,
VcsHandlerParams,
VcsInfo,
} from "./vcs.js"
import { VcsHandler } from "./vcs.js"
import type { GardenError } from "../exceptions.js"
import { ChildProcessError, ConfigurationError, isErrnoException, RuntimeError } from "../exceptions.js"
Expand Down Expand Up @@ -66,6 +74,39 @@ export interface GitCli {
(...args: (string | undefined)[]): Promise<string[]>
}

interface GitSubTreeIncludeExcludeFiles extends BaseIncludeExcludeFiles {
hasIncludes: boolean
}

const getIncludeExcludeFiles: IncludeExcludeFilesHandler<GetFilesParams, GitSubTreeIncludeExcludeFiles> = async (
params: GetFilesParams
) => {
let include = params.include

// We apply the include patterns to the `ls-files` queries. We use the `--glob-pathspecs` flag
// to make sure the path handling is consistent with normal POSIX-style globs used generally by Garden.

// Due to an issue in git, we can unfortunately only use _either_ include or exclude patterns in the
// `ls-files` commands, but not both. Trying both just ignores the exclude patterns.
if (include?.includes("**/*")) {
// This is redundant
include = undefined
}

const hasIncludes = !!include?.length

// Make sure action config is not mutated.
let exclude = !params.exclude ? [] : [...params.exclude]

// It looks like relative paths with redundant '.' and '..' parts
// do not work well along with `--exclude` and `--glob-pathspecs` flags.
// So, we need to normalize paths like './dir' to be just 'dir',
// otherwise such dirs won't be excluded by `--exclude` flag applied with `--glob-pathspecs`.
exclude = [...exclude.map(normalize), "**/.garden/**/*"]

return { include, exclude, hasIncludes }
}

interface Submodule {
path: string
url: string
Expand Down Expand Up @@ -153,19 +194,13 @@ export class GitHandler extends VcsHandler {
* so that {@link getFiles} won't refer to the method in the subclass.
*/
async _getFiles(params: GetFilesParams): Promise<VcsFile[]> {
const { log, path, pathDescription = "directory", filter, failOnPrompt = false } = params
let { include, exclude } = params

if (include && include.length === 0) {
if (params.include && params.include.length === 0) {
// No need to proceed, nothing should be included
return []
}

if (!exclude) {
exclude = []
}
// Make sure action config is not mutated
exclude = [...exclude, "**/.garden/**/*"]
const { log, path, pathDescription = "directory", filter, failOnPrompt = false } = params
const { exclude, hasIncludes, include } = await getIncludeExcludeFiles(params)

const gitLog = log
.createLog({ name: "git" })
Expand Down Expand Up @@ -203,21 +238,6 @@ export class GitHandler extends VcsHandler {
.map((modifiedRelPath) => resolve(gitRoot, modifiedRelPath))
)

const absExcludes = exclude.map((p) => resolve(path, p))

// Apply the include patterns to the ls-files queries. We use the --glob-pathspecs flag
// to make sure the path handling is consistent with normal POSIX-style globs used generally by Garden.

// Due to an issue in git, we can unfortunately only use _either_ include or exclude patterns in the
// ls-files commands, but not both. Trying both just ignores the exclude patterns.

if (include?.includes("**/*")) {
// This is redundant
include = undefined
}

const hasIncludes = !!include?.length

const globalArgs = ["--glob-pathspecs"]
const lsFilesCommonArgs = ["--cached", "--exclude", this.gardenDirPath]

Expand Down Expand Up @@ -256,11 +276,12 @@ export class GitHandler extends VcsHandler {
// Need to automatically add `**/*` to directory paths, to match git behavior when filtering.
const augmentedIncludes = await augmentGlobs(path, include)
const augmentedExcludes = await augmentGlobs(path, exclude)
const absExcludes = exclude.map((p) => resolve(path, p))

// Resolve submodules
// TODO: see about optimizing this, avoiding scans when we're sure they'll not match includes/excludes etc.
submoduleFiles = submodulePaths.map(async (submodulePath) => {
if (!submodulePath.startsWith(path) || absExcludes?.includes(submodulePath)) {
if (!submodulePath.startsWith(path) || absExcludes.includes(submodulePath)) {
return []
}

Expand Down Expand Up @@ -346,7 +367,8 @@ export class GitHandler extends VcsHandler {
return
}

if (hasIncludes && !matchPath(filePath, undefined, exclude)) {
const passesExclusionFilter = matchPath(filePath, undefined, exclude)
if (hasIncludes && !passesExclusionFilter) {
return
}

Expand Down Expand Up @@ -714,8 +736,9 @@ export async function augmentGlobs(basePath: string, globs?: string[]): Promise<
}

try {
const isDir = (await stat(joinWithPosix(basePath, pattern))).isDirectory()
return isDir ? posix.join(pattern, "**", "*") : pattern
const path = joinWithPosix(basePath, pattern)
const stats = await stat(path)
return stats.isDirectory() ? posix.join(pattern, "**", "*") : pattern
} catch {
return pattern
}
Expand Down
10 changes: 10 additions & 0 deletions core/src/vcs/vcs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import type { Garden } from "../garden.js"
import { Profile } from "../util/profiling.js"

import AsyncLock from "async-lock"

const scanLock = new AsyncLock()

export const versionStringPrefix = "v-"
Expand Down Expand Up @@ -121,6 +122,15 @@ export interface GetFilesParams {
scanRoot: string | undefined
}

export interface BaseIncludeExcludeFiles {
include?: string[]
exclude: string[]
}

export type IncludeExcludeFilesHandler<T extends GetFilesParams, R extends BaseIncludeExcludeFiles> = (
params: T
) => Promise<R>

export interface GetTreeVersionParams {
log: Log
projectName: string
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
I'm in the excluded dir. I should be excluded too.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
I'm in the excluded dir. I should be excluded.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
kind: Module
name: module-b
type: test
exclude: ["nope.*"]
# test both dir name and relative path
exclude: ["nope.*", "./dir1", "dir2"]
Loading