diff --git a/core/src/util/fs.ts b/core/src/util/fs.ts index 2850d903f4..1cf6490c29 100644 --- a/core/src/util/fs.ts +++ b/core/src/util/fs.ts @@ -135,6 +135,7 @@ export async function findConfigPathsInPath({ log, filter: (f) => isConfigFilename(basename(f)), scanRoot: dir, + hashUntrackedFiles: false, }) return paths.map((f) => f.path) diff --git a/core/src/vcs/git-repo.ts b/core/src/vcs/git-repo.ts index d0df8493cc..323d4b710c 100644 --- a/core/src/vcs/git-repo.ts +++ b/core/src/vcs/git-repo.ts @@ -27,7 +27,7 @@ import { realpath } from "fs/promises" const { pathExists } = fsExtra -type ScanRepoParams = Pick +type ScanRepoParams = Pick interface GitRepoGetFilesParams extends GetFilesParams { scanFromProjectRoot: boolean @@ -162,6 +162,7 @@ export class GitRepoHandler extends AbstractGitHandler { path: scanRoot, pathDescription: pathDescription || "repository", failOnPrompt, + hashUntrackedFiles: params.hashUntrackedFiles, }) const filesAtPath = fileTree.getFilesAtPath(path) diff --git a/core/src/vcs/git-sub-tree.ts b/core/src/vcs/git-sub-tree.ts index 0c6be2a71e..265b89fadf 100644 --- a/core/src/vcs/git-sub-tree.ts +++ b/core/src/vcs/git-sub-tree.ts @@ -163,7 +163,7 @@ export class GitSubTreeHandler extends AbstractGitHandler { return [] } - const { log, path, pathDescription = "directory", filter, failOnPrompt = false } = params + const { log, path, pathDescription = "directory", filter, failOnPrompt = false, hashUntrackedFiles = true } = params const gitLog = log .createLog({ name: "git" }) @@ -246,6 +246,7 @@ export class GitSubTreeHandler extends AbstractGitHandler { matchPath(join(submoduleRelPath, p), augmentedIncludes, augmentedExcludes) && (!filter || filter(p)), scanRoot: submodulePath, failOnPrompt, + hashUntrackedFiles, }) }) } @@ -290,7 +291,12 @@ export class GitSubTreeHandler extends AbstractGitHandler { // No need to stat unless it has no hash, is a symlink, or is modified // Note: git ls-files always returns mode 120000 for symlinks if (hash && entry.mode !== "120000" && !modifiedFiles.has(resolvedPath)) { - return ensureHash(output, undefined, modifiedFiles) + return ensureHash({ + file: output, + stats: undefined, + modifiedFiles, + hashUntrackedFiles, + }) } try { @@ -313,7 +319,12 @@ export class GitSubTreeHandler extends AbstractGitHandler { gitLog.verbose(`Ignoring symlink pointing outside of ${pathDescription} at ${resolvedPath}`) return } - return ensureHash(output, stats, modifiedFiles) + return ensureHash({ + file: output, + stats, + modifiedFiles, + hashUntrackedFiles, + }) } catch (err) { if (isErrnoException(err) && err.code === "ENOENT") { gitLog.verbose(`Ignoring dead symlink at ${resolvedPath}`) @@ -322,10 +333,20 @@ export class GitSubTreeHandler extends AbstractGitHandler { throw err } } else { - return ensureHash(output, stats, modifiedFiles) + return ensureHash({ + file: output, + stats, + modifiedFiles, + hashUntrackedFiles, + }) } } else { - return ensureHash(output, stats, modifiedFiles) + return ensureHash({ + file: output, + stats, + modifiedFiles, + hashUntrackedFiles, + }) } } catch (err) { if (isErrnoException(err) && err.code === "ENOENT") { @@ -465,21 +486,42 @@ function parseGitLsFilesOutputLine(data: Buffer): GitEntry | undefined { /** * Make sure we have a fresh hash for each file. */ -async function ensureHash( - file: VcsFile, - stats: fsExtra.Stats | undefined, +async function ensureHash({ + file, + stats, + modifiedFiles, + hashUntrackedFiles, +}: { + file: VcsFile + stats: fsExtra.Stats | undefined modifiedFiles: Set -): Promise { - if (file.hash === "" || modifiedFiles.has(file.path)) { - // Don't attempt to hash directories. Directories (which will only come up via symlinks btw) - // will by extension be filtered out of the list. - if (stats && !stats.isDirectory()) { - const hash = await hashObject(stats, file.path) - if (hash !== "") { - file.hash = hash - return file - } + hashUntrackedFiles: boolean +}): Promise { + // If the file has not been modified, then it's either committed or untracked. + if (!modifiedFiles.has(file.path)) { + // If the hash is already defined, then the file is committed and its hash is up-to-date. + if (file.hash !== "") { + return file + } + + // Otherwise, the file is untracked. + if (!hashUntrackedFiles) { + // So we can skip its hash calculation if we don't need the hashes of untracked files. + // Hashes can be skipped while scanning the FS for Garden config files. + return file } } + + // Don't attempt to hash directories. Directories (which will only come up via symlinks btw) + // will by extension be filtered out of the list. + if (!stats || stats.isDirectory()) { + return file + } + + const hash = await hashObject(stats, file.path) + if (hash !== "") { + file.hash = hash + } + return file } diff --git a/core/src/vcs/vcs.ts b/core/src/vcs/vcs.ts index 622ac5af00..395df4c0f3 100644 --- a/core/src/vcs/vcs.ts +++ b/core/src/vcs/vcs.ts @@ -122,6 +122,7 @@ export interface GetFilesParams { filter?: (path: string) => boolean failOnPrompt?: boolean scanRoot: string | undefined + hashUntrackedFiles?: boolean } export interface BaseIncludeExcludeFiles { diff --git a/core/test/unit/src/commands/build.ts b/core/test/unit/src/commands/build.ts index 31efe07a86..b7ec59cb80 100644 --- a/core/test/unit/src/commands/build.ts +++ b/core/test/unit/src/commands/build.ts @@ -22,6 +22,7 @@ import { taskResultOutputs, getAllTaskResults } from "../../../helpers.js" import type { ModuleConfig } from "../../../../src/config/module.js" import type { Log } from "../../../../src/logger/log-entry.js" import fsExtra from "fs-extra" + const { writeFile } = fsExtra import { join } from "path" import type { ProcessCommandResult } from "../../../../src/commands/base.js" @@ -324,10 +325,8 @@ describe("BuildCommand", () => { }) it("should rebuild module if a deep dependency has been modified", async () => { - let garden = await getFreshTestGarden() - const { result: result1 } = await buildCommand.action({ - garden, + garden: await getFreshTestGarden(), ...defaultOpts, args: { names: ["aaa-service"] }, opts: withDefaultGlobalOpts({ "watch": false, "force": true, "with-dependants": false }), @@ -337,8 +336,6 @@ describe("BuildCommand", () => { await writeFile(join(projectPath, "C/file.txt"), "module c has been modified") - garden = await getFreshTestGarden() - const { result: result2 } = await buildCommand.action({ garden: await getFreshTestGarden(), ...defaultOpts,