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: windows file tree #5364

Merged
merged 5 commits into from
Nov 13, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
61 changes: 49 additions & 12 deletions core/src/vcs/file-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,36 @@
*/

import type { VcsFile } from "./vcs.js"
import * as path from "path"
import type { PlatformPath } from "path"
import * as posixPath from "path/posix"
import * as winPath from "path/win32"

type FileTreeNodeOptions = {
ownPath: string
pathUtils: PlatformPath
}

export class FileTreeNode {
private ownPath: string
private pathUtils: PlatformPath
public ownPath: string
private files: VcsFile[] = []
private childrenBySubpath: Map<string, FileTreeNode> = new Map()

constructor({ ownPath }: FileTreeNodeOptions) {
constructor({ ownPath, pathUtils }: FileTreeNodeOptions) {
this.ownPath = ownPath
this.pathUtils = pathUtils
}

addFile(file: VcsFile): boolean {
if (!file.path.startsWith(`/${this.ownPath}`)) {
if (!file.path.startsWith(this.ownPath)) {
return false
}

this.files.push(file)

const relativePath = file.path.slice(this.ownPath.length)
// We use absolute paths so the first part of the split is always an empty string
const [_, subpathSegment, nextSegment] = relativePath.split(path.sep)
const [subpathSegment, nextSegment] = relativePath.split(this.pathUtils.sep)

// We have reached the end of this path
// and arrived at the file.
Expand All @@ -42,7 +47,10 @@ export class FileTreeNode {

let child: FileTreeNode
if (!this.childrenBySubpath.has(subpathSegment)) {
child = new FileTreeNode({ ownPath: path.join(this.ownPath, subpathSegment) })
child = new FileTreeNode({
ownPath: `${this.pathUtils.join(this.ownPath, subpathSegment)}${this.pathUtils.sep}`,
pathUtils: this.pathUtils,
})
this.childrenBySubpath.set(subpathSegment, child)
} else {
child = this.childrenBySubpath.get(subpathSegment)!
Expand All @@ -64,14 +72,18 @@ export class FileTreeNode {

export class FileTree {
private root: FileTreeNode
private pathUtils: PlatformPath

constructor(root: FileTreeNode) {
constructor(root: FileTreeNode, pathUtils: PlatformPath) {
this.root = root
this.pathUtils = pathUtils
}

private getNodeAtPath(filesPath: string): FileTreeNode | undefined {
// TODO: leading slash makes things annoying in many places
const segments = filesPath.slice(1).split(path.sep)
// Since we're always rooted at the same root directory, we remove that from the path we iterate down
// and start out iteration on the root node.
const rootPath = this.pathUtils.parse(filesPath).root
const segments = filesPath.slice(rootPath.length).split(this.pathUtils.sep)

let currentNode: FileTreeNode | undefined = this.root
while (currentNode) {
Expand All @@ -87,6 +99,10 @@ export class FileTree {
}

getFilesAtPath(filesPath: string): VcsFile[] {
if (this.root.ownPath !== this.pathUtils.parse(filesPath).root) {
return []
}

const node = this.getNodeAtPath(filesPath)

if (!node) {
Expand All @@ -104,17 +120,38 @@ export class FileTree {
return node !== undefined
}

static fromFiles(files: VcsFile[]): FileTree {
static fromFiles(files: VcsFile[], platform?: "win32" | "posix"): FileTree {
// In theory, node picks the right utils automatically depending on platform
// However, for testing, we need to be able to specify the platform explicitly here
// else we cannot test for example for Windows on a Unix machine.
const pathUtils = (platform ?? process.platform) === "win32" ? winPath : posixPath
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aaaah, this is awesome! 👏

if (files.length === 0) {
return new FileTree(
new FileTreeNode({
ownPath: "",
pathUtils,
}),
pathUtils
)
}

// We assume all files are rooted from the same directory
// which is the root filesystem directory for most (all?) use cases here.
// If we ever have a case where somehow we have files from different roots,
// we will need to handle that here.
// That probably won't ever be the case on Unix, but on Windows with its drive letter based system,
// it could happen at least theoretically.
// Practically, garden runs and scans from one directory, and things in that cannot be rooted on a different drive.
const rootPath = pathUtils.parse(files[0].path).root
const node = new FileTreeNode({
ownPath: "",
ownPath: rootPath,
pathUtils,
})

for (const file of files) {
node.addFile(file)
}

return new FileTree(node)
return new FileTree(node, pathUtils)
}
}
30 changes: 18 additions & 12 deletions core/src/vcs/git-repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { join } from "path"
import { GitHandler, augmentGlobs } from "./git.js"
import { GitHandler } from "./git.js"
import type { GetFilesParams, VcsFile } from "./vcs.js"
import { isDirectory, joinWithPosix, matchPath } from "../util/fs.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"

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

Expand Down Expand Up @@ -58,11 +58,11 @@ export class GitRepoHandler extends GitHandler {

const moduleFiles = fileTree.getFilesAtPath(path)

const include = params.include ? await absGlobs(path, params.include) : [path, join(path, "**", "*")]
const exclude = await absGlobs(path, params.exclude || [])
const include = params.include ? params.include : ["**/*"]
const exclude = params.exclude || []

if (scanRoot === this.garden?.projectRoot) {
exclude.push(join(scanRoot, ".garden", "**", "*"))
exclude.push("**/.garden/**/*")
}

log.debug(
Expand All @@ -71,7 +71,18 @@ export class GitRepoHandler extends GitHandler {
log.silly(`Include globs: ${include.join(", ")}`)
log.silly(exclude.length > 0 ? `Exclude globs: ${exclude.join(", ")}` : "No exclude globs")

const filtered = moduleFiles.filter(({ path: p }) => (!filter || filter(p)) && matchPath(p, include, exclude))
const filtered = moduleFiles.filter(({ path: p }) => {
if (filter && !filter(p)) {
return false
}
// We remove the subpath from the file path before matching
// so that the globs can be relative to the module path
// Previously we prepended the module path to the globs
// 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)
})

log.debug(`Found ${filtered.length} files in module path after glob matching`)

Expand Down Expand Up @@ -112,8 +123,3 @@ export class GitRepoHandler extends GitHandler {
})
}
}

async function absGlobs(basePath: string, globs: string[]): Promise<string[]> {
const augmented = await augmentGlobs(basePath, globs)
return augmented?.map((p) => joinWithPosix(basePath, p)) || []
}
72 changes: 72 additions & 0 deletions core/test/unit/src/vcs/file-tree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (C) 2018-2023 Garden Technologies, Inc. <info@garden.io>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { expect } from "chai"
import { FileTree } from "../../../../src/vcs/file-tree.js"

describe("file-tree", () => {
describe("on unix", () => {
it("should get the files at a path", () => {
const fileTree = FileTree.fromFiles(
[
{ path: "/Users/developer/code/garden-project/project-1/frontend", hash: "" },
{ path: "/Users/developer/code/garden-project/project-1/backend", hash: "" },
{ path: "/Users/developer/code/garden-project/project-2/frontend", hash: "" },
{ path: "/Users/developer/code/garden-project/project-2/backend", hash: "" },
],
"posix"
)

const filesAtProjectPath = fileTree.getFilesAtPath("/Users/developer/code/garden-project")

expect(filesAtProjectPath).to.eql([
{ path: "/Users/developer/code/garden-project/project-1/frontend", hash: "" },
{ path: "/Users/developer/code/garden-project/project-1/backend", hash: "" },
{ path: "/Users/developer/code/garden-project/project-2/frontend", hash: "" },
{ path: "/Users/developer/code/garden-project/project-2/backend", hash: "" },
])

const filesAtFirstProjectPath = fileTree.getFilesAtPath("/Users/developer/code/garden-project/project-1")

expect(filesAtFirstProjectPath).to.eql([
{ path: "/Users/developer/code/garden-project/project-1/frontend", hash: "" },
{ path: "/Users/developer/code/garden-project/project-1/backend", hash: "" },
])
})
})

describe("on windows", () => {
it("should get the files at a path", () => {
const fileTree = FileTree.fromFiles(
[
{ path: "C:\\Users\\developer\\code\\garden-project\\project-1\\frontend", hash: "" },
{ path: "C:\\Users\\developer\\code\\garden-project\\project-1\\backend", hash: "" },
{ path: "C:\\Users\\developer\\code\\garden-project\\project-2\\frontend", hash: "" },
{ path: "C:\\Users\\developer\\code\\garden-project\\project-2\\backend", hash: "" },
],
"win32"
)

const filesAtProjectPath = fileTree.getFilesAtPath("C:\\Users\\developer\\code\\garden-project")

expect(filesAtProjectPath).to.eql([
{ path: "C:\\Users\\developer\\code\\garden-project\\project-1\\frontend", hash: "" },
{ path: "C:\\Users\\developer\\code\\garden-project\\project-1\\backend", hash: "" },
{ path: "C:\\Users\\developer\\code\\garden-project\\project-2\\frontend", hash: "" },
{ path: "C:\\Users\\developer\\code\\garden-project\\project-2\\backend", hash: "" },
])

const filesAtFirstProjectPath = fileTree.getFilesAtPath("C:\\Users\\developer\\code\\garden-project\\project-1")

expect(filesAtFirstProjectPath).to.eql([
{ path: "C:\\Users\\developer\\code\\garden-project\\project-1\\frontend", hash: "" },
{ path: "C:\\Users\\developer\\code\\garden-project\\project-1\\backend", hash: "" },
])
})
})
})