diff --git a/packages/databricks-vscode/src/bundle/BundleFileSet.test.ts b/packages/databricks-vscode/src/bundle/BundleFileSet.test.ts index 05bf2a275..ef5889066 100644 --- a/packages/databricks-vscode/src/bundle/BundleFileSet.test.ts +++ b/packages/databricks-vscode/src/bundle/BundleFileSet.test.ts @@ -20,10 +20,10 @@ describe(__filename, async function () { await tmpdir.cleanup(); }); - function getWorkspaceFolderManagerMock() { + function getWorkspaceFolderManagerMock(projectDir?: string) { const mockWorkspaceFolderManager = mock(); const mockWorkspaceFolder = mock(); - const uri = Uri.file(tmpdir.path); + const uri = Uri.file(projectDir ?? tmpdir.path); when(mockWorkspaceFolder.uri).thenReturn(uri); when(mockWorkspaceFolderManager.activeWorkspaceFolder).thenReturn( instance(mockWorkspaceFolder) @@ -73,6 +73,71 @@ describe(__filename, async function () { expect(await bundleFileSet.getRootFile()).to.be.undefined; }); + describe("parent-directory includes", async () => { + it("getIncludedFiles should find files referenced via .. paths", async () => { + // Structure: tmpdir/shared/config.yml (included), tmpdir/project/sub/ (project root) + const sharedDir = path.join(tmpdir.path, "shared"); + const projectDir = path.join(tmpdir.path, "project", "sub"); + await fs.mkdir(sharedDir, {recursive: true}); + await fs.mkdir(projectDir, {recursive: true}); + + const sharedFile = path.join(sharedDir, "config.yml"); + const sharedFile2 = path.join(sharedDir, "config2.yml"); + await fs.writeFile(sharedFile, ""); + await fs.writeFile(sharedFile2, ""); + + const rootBundleData: BundleSchema = { + include: ["../../shared/config.yml", "../../shared/config2.yml"], + }; + await fs.writeFile( + path.join(projectDir, "databricks.yml"), + yaml.stringify(rootBundleData) + ); + + const bundleFileSet = new BundleFileSet( + getWorkspaceFolderManagerMock(projectDir) + ); + + const files = await bundleFileSet.getIncludedFiles(); + expect(files).to.not.be.undefined; + expect(files!.map((f) => f.fsPath).sort()).to.deep.equal( + [sharedFile, sharedFile2].sort() + ); + }); + + it("isIncludedBundleFile should return true for files referenced via .. paths", async () => { + const sharedDir = path.join(tmpdir.path, "shared"); + const projectDir = path.join(tmpdir.path, "project", "sub"); + await fs.mkdir(sharedDir, {recursive: true}); + await fs.mkdir(projectDir, {recursive: true}); + + const sharedFile = path.join(sharedDir, "config.yml"); + await fs.writeFile(sharedFile, ""); + + const rootBundleData: BundleSchema = { + include: ["../../shared/config.yml", "local.yml"], + }; + await fs.writeFile( + path.join(projectDir, "databricks.yml"), + yaml.stringify(rootBundleData) + ); + + const bundleFileSet = new BundleFileSet( + getWorkspaceFolderManagerMock(projectDir) + ); + + expect( + await bundleFileSet.isIncludedBundleFile(Uri.file(sharedFile)) + ).to.be.true; + + expect( + await bundleFileSet.isIncludedBundleFile( + Uri.file(path.join(projectDir, "other.yml")) + ) + ).to.be.false; + }); + }); + describe("file listing", async () => { beforeEach(async () => { const rootBundleData: BundleSchema = { diff --git a/packages/databricks-vscode/src/bundle/BundleFileSet.ts b/packages/databricks-vscode/src/bundle/BundleFileSet.ts index f362e004e..641a65765 100644 --- a/packages/databricks-vscode/src/bundle/BundleFileSet.ts +++ b/packages/databricks-vscode/src/bundle/BundleFileSet.ts @@ -88,33 +88,47 @@ export class BundleFileSet { return Uri.file(rootFile[0]); } - async getIncludedFilesGlob() { + private async getIncludePatterns(): Promise { const rootFile = await this.getRootFile(); if (rootFile === undefined) { - return undefined; + return []; + } + const bundle = await parseBundleYaml(rootFile); + if (!bundle?.include?.length) { + return []; } - const bundle = await parseBundleYaml(Uri.file(rootFile.fsPath)); - if (bundle?.include === undefined || bundle?.include.length === 0) { + return bundle.include; + } + + async getIncludedFilesGlob() { + const patterns = await this.getIncludePatterns(); + if (patterns.length === 0) { return undefined; } - if (bundle?.include.length === 1) { - return bundle.include[0]; + if (patterns.length === 1) { + return patterns[0]; } - return `{${bundle.include.join(",")}}`; + return `{${patterns.join(",")}}`; } async getIncludedFiles() { - const includedFilesGlob = await this.getIncludedFilesGlob(); - if (includedFilesGlob !== undefined) { - return ( - await glob.glob( - toGlobPath( - path.join(this.projectRoot.fsPath, includedFilesGlob) - ), - {nocase: process.platform === "win32"} - ) - ).map((i) => Uri.file(i)); + const patterns = await this.getIncludePatterns(); + if (patterns.length === 0) { + return undefined; + } + + const allFiles: string[] = []; + for (const pattern of patterns) { + const absolutePattern = toGlobPath( + path.resolve(this.projectRoot.fsPath, pattern) + ); + const files = await glob.glob(absolutePattern, { + nocase: process.platform === "win32", + }); + allFiles.push(...files); } + + return [...new Set(allFiles)].map((f) => Uri.file(f)); } async allFiles() { @@ -152,15 +166,16 @@ export class BundleFileSet { } async isIncludedBundleFile(e: Uri) { - let includedFilesGlob = await this.getIncludedFilesGlob(); - if (includedFilesGlob === undefined) { - return false; + const patterns = await this.getIncludePatterns(); + for (const pattern of patterns) { + const absolutePattern = toGlobPath( + path.resolve(this.projectRoot.fsPath, pattern) + ); + if (minimatch(toGlobPath(e.fsPath), absolutePattern)) { + return true; + } } - includedFilesGlob = getAbsoluteGlobPath( - includedFilesGlob, - this.projectRoot - ); - return minimatch(e.fsPath, toGlobPath(includedFilesGlob)); + return false; } async isBundleFile(e: Uri) {