diff --git a/.gardenignore b/.gardenignore index 0c76a8cf09..5b72f265a7 100644 --- a/.gardenignore +++ b/.gardenignore @@ -1,2 +1,2 @@ examples/ -test/ +node_modules/ diff --git a/docs/reference/config.md b/docs/reference/config.md index 7f53faeaec..ccf34d6a5a 100644 --- a/docs/reference/config.md +++ b/docs/reference/config.md @@ -279,6 +279,30 @@ module: | Type | Required | | ---- | -------- | | `string` | No +### `module.include[]` +[module](#module) > include + +Specify a list of POSIX-style paths or globs that should be regarded as the source files for this +module. Files that do *not* match these paths or globs are excluded when computing the version of the module, +as well as when responding to filesystem watch events. + +Note that you can also _exclude_ files by placing `.gardenignore` files in your source tree, which use the +same format as `.gitignore` files. + +Also note that specifying an empty list here means _no sources_ should be included. + +| Type | Required | +| ---- | -------- | +| `array[string]` | No + +Example: +```yaml +module: + ... + include: + - Dockerfile + - my-app.js +``` ### `module.repositoryUrl` [module](#module) > repositoryUrl @@ -372,6 +396,7 @@ module: type: name: description: + include: repositoryUrl: allowPublish: true build: diff --git a/garden-service/bin/add-version-files.ts b/garden-service/bin/add-version-files.ts index d79a988516..9f3ffa3d70 100755 --- a/garden-service/bin/add-version-files.ts +++ b/garden-service/bin/add-version-files.ts @@ -24,7 +24,7 @@ async function addVersionFiles() { const versionFilePath = resolve(path, ".garden-version") const vcsHandler = new GitHandler(path) - const treeVersion = await vcsHandler.getTreeVersion(path) + const treeVersion = await vcsHandler.getTreeVersion(path, config.include || null) console.log(`${config.name} -> ${relative(staticPath, versionFilePath)}`) diff --git a/garden-service/package-lock.json b/garden-service/package-lock.json index 80561dfd55..70864aa4aa 100644 --- a/garden-service/package-lock.json +++ b/garden-service/package-lock.json @@ -3808,7 +3808,8 @@ "ansi-regex": { "version": "2.1.1", "resolved": false, - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true }, "aproba": { "version": "1.2.0", @@ -3829,12 +3830,14 @@ "balanced-match": { "version": "1.0.0", "resolved": false, - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "optional": true }, "brace-expansion": { "version": "1.1.11", "resolved": false, "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3849,17 +3852,20 @@ "code-point-at": { "version": "1.1.0", "resolved": false, - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "optional": true }, "concat-map": { "version": "0.0.1", "resolved": false, - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "optional": true }, "console-control-strings": { "version": "1.1.0", "resolved": false, - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3976,7 +3982,8 @@ "inherits": { "version": "2.0.3", "resolved": false, - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "optional": true }, "ini": { "version": "1.3.5", @@ -3988,6 +3995,7 @@ "version": "1.0.0", "resolved": false, "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4002,6 +4010,7 @@ "version": "3.0.4", "resolved": false, "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4009,12 +4018,14 @@ "minimist": { "version": "0.0.8", "resolved": false, - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "optional": true }, "minipass": { "version": "2.2.4", "resolved": false, "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -4033,6 +4044,7 @@ "version": "0.5.1", "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "optional": true, "requires": { "minimist": "0.0.8" } @@ -4113,7 +4125,8 @@ "number-is-nan": { "version": "1.0.1", "resolved": false, - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4125,6 +4138,7 @@ "version": "1.4.0", "resolved": false, "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "optional": true, "requires": { "wrappy": "1" } @@ -4210,7 +4224,8 @@ "safe-buffer": { "version": "5.1.1", "resolved": false, - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -4246,6 +4261,7 @@ "version": "1.0.2", "resolved": false, "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4265,6 +4281,7 @@ "version": "3.0.1", "resolved": false, "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4308,12 +4325,14 @@ "wrappy": { "version": "1.0.2", "resolved": false, - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "optional": true }, "yallist": { "version": "3.0.2", "resolved": false, - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", + "optional": true } } }, @@ -7498,6 +7517,7 @@ "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "dev": true, + "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -7867,7 +7887,8 @@ "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true + "dev": true, + "optional": true }, "is-builtin-module": { "version": "1.0.0", @@ -7963,6 +7984,7 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, + "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -8015,7 +8037,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true + "dev": true, + "optional": true }, "lru-cache": { "version": "4.1.3", @@ -8318,7 +8341,8 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true + "dev": true, + "optional": true }, "require-directory": { "version": "2.1.1", diff --git a/garden-service/package.json b/garden-service/package.json index 23322f3e48..f450826401 100644 --- a/garden-service/package.json +++ b/garden-service/package.json @@ -69,6 +69,7 @@ "koa-websocket": "^5.0.1", "lodash": "^4.17.11", "log-symbols": "^2.2.0", + "minimatch": "^3.0.4", "mocha-logger": "^1.0.6", "moment": "^2.23.0", "node-emoji": "^1.8.1", @@ -125,6 +126,7 @@ "@types/lodash": "^4.14.119", "@types/log-symbols": "^2.0.0", "@types/log-update": "^2.0.0", + "@types/minimatch": "^3.0.3", "@types/mocha": "^5.2.5", "@types/nock": "^9.3.0", "@types/node": "^10.12.15", diff --git a/garden-service/src/build-dir.ts b/garden-service/src/build-dir.ts index b18dcc9149..6855606e21 100644 --- a/garden-service/src/build-dir.ts +++ b/garden-service/src/build-dir.ts @@ -29,7 +29,7 @@ import { import { zip } from "lodash" import * as execa from "execa" import { platform } from "os" -import { toCygwinPath } from "./util/util" +import { toCygwinPath } from "./util/fs" import { ModuleConfig } from "./config/module" import { LogEntry } from "./logger/log-entry" diff --git a/garden-service/src/commands/publish.ts b/garden-service/src/commands/publish.ts index 29216e533f..ed783e4f54 100644 --- a/garden-service/src/commands/publish.ts +++ b/garden-service/src/commands/publish.ts @@ -16,7 +16,6 @@ import { } from "./base" import { Module } from "../types/module" import { PublishTask } from "../tasks/publish" -import { RuntimeError } from "../exceptions" import { TaskResults } from "../task-graph" import { Garden } from "../garden" import { LogEntry } from "../logger/log-entry" @@ -80,17 +79,11 @@ export async function publishModules( forceBuild: boolean, allowDirty: boolean, ): Promise { - const tasks = modules.map(module => { - const version = module.version - - if (version.dirtyTimestamp && !allowDirty) { - throw new RuntimeError( - `Module ${module.name} has uncommitted changes. ` + - `Please commit them, clean the module's source tree or set the --allow-dirty flag to override.`, - { moduleName: module.name, version }, - ) - } + if (!!allowDirty) { + log.warn(`The --allow-dirty flag has been deprecated. It no longer has an effect.`) + } + const tasks = modules.map(module => { return new PublishTask({ garden, log, module, forceBuild }) }) diff --git a/garden-service/src/commands/update-remote/helpers.ts b/garden-service/src/commands/update-remote/helpers.ts index 2832353efb..8f588ccdc6 100644 --- a/garden-service/src/commands/update-remote/helpers.ts +++ b/garden-service/src/commands/update-remote/helpers.ts @@ -10,7 +10,7 @@ import { difference } from "lodash" import { join, basename } from "path" import { remove, pathExists } from "fs-extra" -import { getChildDirNames } from "../../util/util" +import { getChildDirNames } from "../../util/fs" import { ExternalSourceType, getRemoteSourcesDirname, diff --git a/garden-service/src/config/base.ts b/garden-service/src/config/base.ts index 2e0e82cfbe..583d30647f 100644 --- a/garden-service/src/config/base.ts +++ b/garden-service/src/config/base.ts @@ -7,10 +7,7 @@ */ import { join, basename, sep, resolve, relative } from "path" -import { - findByName, - getNames, -} from "../util/util" +import { findByName, getNames } from "../util/util" import * as Joi from "joi" import * as yaml from "js-yaml" import { readFile } from "fs-extra" @@ -19,8 +16,7 @@ import { baseModuleSpecSchema, ModuleConfig } from "./module" import { validateWithPath } from "./common" import { ConfigurationError } from "../exceptions" import { defaultEnvironments, ProjectConfig, projectSchema } from "../config/project" - -const CONFIG_FILENAME = "garden.yml" +import { CONFIG_FILENAME } from "../constants" export interface GardenConfig { dirname: string @@ -243,6 +239,7 @@ function prepareModuleConfig(moduleSpec: any, path: string): ModuleConfig { dependencies, }, description: moduleSpec.description, + include: moduleSpec.include, name: moduleSpec.name, outputs: {}, path, diff --git a/garden-service/src/config/config-context.ts b/garden-service/src/config/config-context.ts index 5a4f30a0e8..519a8fdcb2 100644 --- a/garden-service/src/config/config-context.ts +++ b/garden-service/src/config/config-context.ts @@ -14,7 +14,7 @@ import { ConfigurationError } from "../exceptions" import { resolveTemplateString } from "../template-string" import * as Joi from "joi" import { Garden } from "../garden" -import { ModuleVersion } from "../vcs/base" +import { ModuleVersion } from "../vcs/vcs" export type ContextKey = string[] diff --git a/garden-service/src/config/module.ts b/garden-service/src/config/module.ts index 3c3bd38ecd..7d9a455bda 100644 --- a/garden-service/src/config/module.ts +++ b/garden-service/src/config/module.ts @@ -69,6 +69,7 @@ export interface BaseModuleSpec { allowPublish: boolean build: BaseBuildSpec description?: string + include?: string[] name: string path: string type: string @@ -102,6 +103,17 @@ export const baseModuleSpecSchema = Joi.object() .description("The name of this module.") .example("my-sweet-module"), description: Joi.string(), + include: Joi.array().items(Joi.string().uri({ relativeOnly: true })) + .description( + dedent`Specify a list of POSIX-style paths or globs that should be regarded as the source files for this + module. Files that do *not* match these paths or globs are excluded when computing the version of the module, + as well as when responding to filesystem watch events. + + Note that you can also _exclude_ files by placing \`.gardenignore\` files in your source tree, which use the + same format as \`.gitignore\` files. + + Also note that specifying an empty list here means _no sources_ should be included.`) + .example([["Dockerfile", "my-app.js"], {}]), repositoryUrl: joiRepositoryUrl() .description( dedent`${joiRepositoryUrl().describe().description} diff --git a/garden-service/src/constants.ts b/garden-service/src/constants.ts index 80919370ec..9d3bb8ad1f 100644 --- a/garden-service/src/constants.ts +++ b/garden-service/src/constants.ts @@ -10,7 +10,7 @@ import { resolve, join } from "path" export const isPkg = !!(process).pkg -export const MODULE_CONFIG_FILENAME = "garden.yml" +export const CONFIG_FILENAME = "garden.yml" export const LOCAL_CONFIG_FILENAME = "local-config.yml" export const STATIC_DIR = resolve(isPkg ? process.execPath : __dirname, "..", "static") // We copy the built dashboard to the garden-service static directory (with gulp in development, otherwise in CI). diff --git a/garden-service/src/events.ts b/garden-service/src/events.ts index f80dbdc38f..68ef42322a 100644 --- a/garden-service/src/events.ts +++ b/garden-service/src/events.ts @@ -8,7 +8,7 @@ import { EventEmitter2 } from "eventemitter2" import { TaskResult } from "./task-graph" -import { ModuleVersion } from "./vcs/base" +import { ModuleVersion } from "./vcs/vcs" /** * This simple class serves as the central event bus for a Garden instance. Its function diff --git a/garden-service/src/garden.ts b/garden-service/src/garden.ts index d87c667d83..9fafa074e3 100644 --- a/garden-service/src/garden.ts +++ b/garden-service/src/garden.ts @@ -31,22 +31,15 @@ import { builtinPlugins, fixedPlugins } from "./plugins/plugins" import { Module, getModuleCacheContext, getModuleKey, ModuleConfigMap } from "./types/module" import { moduleActionNames, pluginModuleSchema, pluginSchema } from "./types/plugin/plugin" import { Environment, SourceConfig, ProviderConfig, Provider } from "./config/project" -import { - findByName, - getIgnorer, - getNames, - scanDirectory, - pickKeys, - Ignorer, -} from "./util/util" -import { DEFAULT_NAMESPACE, MODULE_CONFIG_FILENAME } from "./constants" +import { findByName, getNames, pickKeys } from "./util/util" +import { DEFAULT_NAMESPACE, CONFIG_FILENAME } from "./constants" import { ConfigurationError, ParameterError, PluginError, RuntimeError, } from "./exceptions" -import { VcsHandler, ModuleVersion } from "./vcs/base" +import { VcsHandler, ModuleVersion } from "./vcs/vcs" import { GitHandler } from "./vcs/git" import { BuildDir } from "./build-dir" import { ConfigGraph } from "./config-graph" @@ -74,6 +67,7 @@ import { platform, arch } from "os" import { LogEntry } from "./logger/log-entry" import { EventBus } from "./events" import { Watcher } from "./watch" +import { getIgnorer, Ignorer, scanDirectory } from "./util/fs" export interface ActionHandlerMap { [actionName: string]: PluginActions[T] @@ -608,7 +602,7 @@ export class Garden { const parsedPath = parse(item.path) - if (parsedPath.base !== MODULE_CONFIG_FILENAME) { + if (parsedPath.base !== CONFIG_FILENAME) { continue } @@ -651,8 +645,8 @@ export class Garden { if (this.moduleConfigs[key]) { const [pathA, pathB] = [ - relative(this.projectRoot, join(this.moduleConfigs[key].path, MODULE_CONFIG_FILENAME)), - relative(this.projectRoot, join(config.path, MODULE_CONFIG_FILENAME)), + relative(this.projectRoot, join(this.moduleConfigs[key].path, CONFIG_FILENAME)), + relative(this.projectRoot, join(config.path, CONFIG_FILENAME)), ].sort() throw new ConfigurationError( diff --git a/garden-service/src/plugins/exec.ts b/garden-service/src/plugins/exec.ts index 2b0df09f4f..a62c2ee020 100644 --- a/garden-service/src/plugins/exec.ts +++ b/garden-service/src/plugins/exec.ts @@ -34,7 +34,7 @@ import { } from "../types/plugin/params" import { CommonServiceSpec } from "../config/service" import { BaseTestSpec, baseTestSpecSchema } from "../config/test" -import { readModuleVersionFile, writeModuleVersionFile, ModuleVersion } from "../vcs/base" +import { readModuleVersionFile, writeModuleVersionFile, ModuleVersion } from "../vcs/vcs" import { GARDEN_BUILD_VERSION_FILENAME } from "../constants" import { ModuleSpec, BaseBuildSpec, baseBuildSpecSchema } from "../config/module" import execa = require("execa") diff --git a/garden-service/src/plugins/kubernetes/task-results.ts b/garden-service/src/plugins/kubernetes/task-results.ts index 82138b70fc..00b7380e58 100644 --- a/garden-service/src/plugins/kubernetes/task-results.ts +++ b/garden-service/src/plugins/kubernetes/task-results.ts @@ -9,7 +9,7 @@ import { GetTaskResultParams } from "../../types/plugin/params" import { ContainerModule } from "../container/config" import { HelmModule } from "./helm/config" -import { ModuleVersion } from "../../vcs/base" +import { ModuleVersion } from "../../vcs/vcs" import { KubernetesPluginContext, KubernetesProvider } from "./kubernetes" import { KubeApi } from "./api" import { getMetadataNamespace } from "./namespace" diff --git a/garden-service/src/plugins/kubernetes/test.ts b/garden-service/src/plugins/kubernetes/test.ts index 65bf03e372..990add4448 100644 --- a/garden-service/src/plugins/kubernetes/test.ts +++ b/garden-service/src/plugins/kubernetes/test.ts @@ -12,7 +12,7 @@ import { ContainerModule } from "../container/config" import { deserializeValues, serializeValues } from "../../util/util" import { KubeApi } from "./api" import { Module } from "../../types/module" -import { ModuleVersion } from "../../vcs/base" +import { ModuleVersion } from "../../vcs/vcs" import { HelmModule } from "./helm/config" import { PluginContext } from "../../plugin-context" import { KubernetesPluginContext } from "./kubernetes" diff --git a/garden-service/src/tasks/base.ts b/garden-service/src/tasks/base.ts index 29f064ad8d..47f5bd89cf 100644 --- a/garden-service/src/tasks/base.ts +++ b/garden-service/src/tasks/base.ts @@ -7,7 +7,7 @@ */ import { TaskResults } from "../task-graph" -import { ModuleVersion } from "../vcs/base" +import { ModuleVersion } from "../vcs/vcs" import { v1 as uuidv1 } from "uuid" import { Garden } from "../garden" import { DependencyGraphNodeType } from "../config-graph" diff --git a/garden-service/src/tasks/task.ts b/garden-service/src/tasks/task.ts index 1557184b9d..37c752f43f 100644 --- a/garden-service/src/tasks/task.ts +++ b/garden-service/src/tasks/task.ts @@ -17,7 +17,7 @@ import { LogEntry } from "../logger/log-entry" import { RunTaskResult } from "../types/plugin/outputs" import { prepareRuntimeContext } from "../types/service" import { DependencyGraphNodeType, ConfigGraph } from "../config-graph" -import { ModuleVersion } from "../vcs/base" +import { ModuleVersion } from "../vcs/vcs" export interface TaskTaskParams { garden: Garden diff --git a/garden-service/src/tasks/test.ts b/garden-service/src/tasks/test.ts index 0970afa4a3..06d519d849 100644 --- a/garden-service/src/tasks/test.ts +++ b/garden-service/src/tasks/test.ts @@ -10,7 +10,7 @@ import * as Bluebird from "bluebird" import chalk from "chalk" import { Module } from "../types/module" import { TestConfig } from "../config/test" -import { ModuleVersion } from "../vcs/base" +import { ModuleVersion } from "../vcs/vcs" import { PushTask } from "./push" import { DeployTask } from "./deploy" import { TestResult } from "../types/plugin/outputs" diff --git a/garden-service/src/types/module.ts b/garden-service/src/types/module.ts index a68f09619e..e91b1d7144 100644 --- a/garden-service/src/types/module.ts +++ b/garden-service/src/types/module.ts @@ -6,19 +6,21 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +import { resolve } from "path" import { flatten, uniq, cloneDeep, keyBy } from "lodash" import { getNames } from "../util/util" import { TestSpec } from "../config/test" import { ModuleSpec, ModuleConfig, moduleConfigSchema } from "../config/module" import { ServiceSpec } from "../config/service" import { TaskSpec } from "../config/task" -import { ModuleVersion, moduleVersionSchema } from "../vcs/base" +import { ModuleVersion, moduleVersionSchema } from "../vcs/vcs" import { pathToCacheContext } from "../cache" import { Garden } from "../garden" import * as Joi from "joi" import { joiArray, joiIdentifier, joiIdentifierMap } from "../config/common" import { ConfigGraph } from "../config-graph" import * as Bluebird from "bluebird" +import { CONFIG_FILENAME } from "../constants" export interface FileCopySpec { source: string @@ -31,8 +33,11 @@ export interface Module< T extends TestSpec = any, W extends TaskSpec = any, > extends ModuleConfig { + buildPath: string buildMetadataPath: string + configPath: string + version: ModuleVersion buildDependencies: ModuleMap @@ -56,6 +61,10 @@ export const moduleSchema = moduleConfigSchema .required() .uri({ relativeOnly: true }) .description("The path to the build metadata directory for the module."), + configPath: Joi.string() + .required() + .uri({ relativeOnly: true }) + .description("The path to the module config file."), version: moduleVersionSchema .required(), buildDependencies: joiIdentifierMap(Joi.lazy(() => moduleSchema)) @@ -84,12 +93,22 @@ export interface ModuleConfigMap { } export async function moduleFromConfig(garden: Garden, graph: ConfigGraph, config: ModuleConfig): Promise { + const configPath = resolve(config.path, CONFIG_FILENAME) + const version = await garden.resolveVersion(config.name, config.build.dependencies) + + // Always include configuration file + if (!version.files.includes(configPath)) { + version.files.push(configPath) + } + const module: Module = { ...cloneDeep(config), buildPath: await garden.buildDir.buildPath(config.name), buildMetadataPath: await garden.buildDir.buildMetadataPath(config.name), - version: await garden.resolveVersion(config.name, config.build.dependencies), + configPath, + + version, buildDependencies: {}, diff --git a/garden-service/src/types/plugin/outputs.ts b/garden-service/src/types/plugin/outputs.ts index 3b7fe228b7..b2bfed2c2c 100644 --- a/garden-service/src/types/plugin/outputs.ts +++ b/garden-service/src/types/plugin/outputs.ts @@ -7,7 +7,7 @@ */ import * as Joi from "joi" -import { ModuleVersion, moduleVersionSchema } from "../../vcs/base" +import { ModuleVersion, moduleVersionSchema } from "../../vcs/vcs" import { Module } from "../module" import { ServiceStatus } from "../service" import { moduleConfigSchema, ModuleConfig } from "../../config/module" diff --git a/garden-service/src/types/plugin/params.ts b/garden-service/src/types/plugin/params.ts index 75b1d76c8b..ebbd305109 100644 --- a/garden-service/src/types/plugin/params.ts +++ b/garden-service/src/types/plugin/params.ts @@ -10,7 +10,7 @@ import * as Joi from "joi" import Stream from "ts-stream" import { LogEntry } from "../../logger/log-entry" import { PluginContext, pluginContextSchema } from "../../plugin-context" -import { ModuleVersion, moduleVersionSchema } from "../../vcs/base" +import { ModuleVersion, moduleVersionSchema } from "../../vcs/vcs" import { Primitive, joiPrimitive, joiArray } from "../../config/common" import { Module, moduleSchema } from "../module" import { RuntimeContext, Service, serviceSchema, runtimeContextSchema } from "../service" diff --git a/garden-service/src/types/service.ts b/garden-service/src/types/service.ts index 13e94c6f04..2b57c58285 100644 --- a/garden-service/src/types/service.ts +++ b/garden-service/src/types/service.ts @@ -14,7 +14,7 @@ import { serviceOutputsSchema, ServiceConfig, serviceConfigSchema } from "../con import { validate } from "../config/common" import dedent = require("dedent") import { format } from "url" -import { moduleVersionSchema } from "../vcs/base" +import { moduleVersionSchema } from "../vcs/vcs" import { Garden } from "../garden" import { uniq } from "lodash" import { ConfigGraph } from "../config-graph" diff --git a/garden-service/src/util/fs.ts b/garden-service/src/util/fs.ts new file mode 100644 index 0000000000..d24e733694 --- /dev/null +++ b/garden-service/src/util/fs.ts @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2018 Garden Technologies, Inc. + * + * 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 klaw = require("klaw") +import * as _spawn from "cross-spawn" +import { pathExists, readFile } from "fs-extra" +import minimatch = require("minimatch") +import { some } from "lodash" +import { join, basename, win32, posix } from "path" +import { GARDEN_DIR_NAME } from "../constants" +// NOTE: Importing from ignore/ignore doesn't work on Windows +const ignore = require("ignore") + +/* + Warning: Don't make any async calls in the loop body when using this function, since this may cause + funky concurrency behavior. + */ +export async function* scanDirectory(path: string, opts?: klaw.Options): AsyncIterableIterator { + let done = false + let resolver + let rejecter + + klaw(path, opts) + .on("data", (item) => { + if (item.path !== path) { + resolver(item) + } + }) + .on("error", (err) => { + rejecter(err) + }) + .on("end", () => { + done = true + resolver() + }) + + // a nice little trick to turn the stream into an async generator + while (!done) { + const promise: Promise = new Promise((resolve, reject) => { + resolver = resolve + rejecter = reject + }) + + yield await promise + } +} + +export async function getChildDirNames(parentDir: string): Promise { + let dirNames: string[] = [] + // Filter on hidden dirs by default. We could make the filter function a param if needed later + const filter = (item: string) => !basename(item).startsWith(".") + + for await (const item of scanDirectory(parentDir, { depthLimit: 0, filter })) { + if (!item || !item.stats.isDirectory()) { + continue + } + dirNames.push(basename(item.path)) + } + return dirNames +} + +export interface Ignorer { + ignores: (path: string) => boolean +} + +export async function getIgnorer(rootPath: string): Promise { + // TODO: this doesn't handle nested .gitignore files, we should revisit + const gitignorePath = join(rootPath, ".gitignore") + const gardenignorePath = join(rootPath, ".gardenignore") + const ig = ignore() + + if (await pathExists(gitignorePath)) { + ig.add((await readFile(gitignorePath)).toString()) + } + + if (await pathExists(gardenignorePath)) { + ig.add((await readFile(gardenignorePath)).toString()) + } + + // should we be adding this (or more) by default? + ig.add([ + "node_modules", + ".git", + "*.log", + GARDEN_DIR_NAME, + // TODO Take a better look at the temp files mutagen creates + ".mutagen-*", + ]) + + return ig +} + +/** + * Converts a Windows-style path to a cygwin style path (e.g. C:\some\folder -> /cygdrive/c/some/folder). + */ +export function toCygwinPath(path: string) { + const parsed = win32.parse(path) + const drive = parsed.root.split(":")[0].toLowerCase() + const dirs = parsed.dir.split(win32.sep).slice(1) + const cygpath = posix.join("/cygdrive", drive, ...dirs, parsed.base) + + // make sure trailing slash is retained + return path.endsWith(win32.sep) ? cygpath + posix.sep : cygpath +} + +/** + * Checks if the given `path` matches any of the given glob `patterns`. + */ +export function matchGlobs(path: string, patterns: string[]): boolean { + return some(patterns, pattern => minimatch(path, pattern)) +} diff --git a/garden-service/src/util/util.ts b/garden-service/src/util/util.ts index 19f1a014e6..8be49a7032 100644 --- a/garden-service/src/util/util.ts +++ b/garden-service/src/util/util.ts @@ -9,22 +9,17 @@ import Bluebird = require("bluebird") import { ResolvableProps } from "bluebird" import * as exitHook from "async-exit-hook" -import * as klaw from "klaw" import * as yaml from "js-yaml" import * as Cryo from "cryo" import * as _spawn from "cross-spawn" -import { pathExists, readFile, writeFile } from "fs-extra" -import { join, basename, win32, posix } from "path" +import { readFile, writeFile } from "fs-extra" import { find, pick, difference, fromPairs, uniqBy } from "lodash" import { TimeoutError, ParameterError, RuntimeError, GardenError } from "../exceptions" import { isArray, isPlainObject, extend, mapValues, pickBy } from "lodash" import highlight from "cli-highlight" import chalk from "chalk" import { safeDump } from "js-yaml" -import { GARDEN_DIR_NAME } from "../constants" import { createHash } from "crypto" -// NOTE: Importing from ignore/ignore doesn't work on Windows -const ignore = require("ignore") // shim to allow async generator functions if (typeof (Symbol as any).asyncIterator === "undefined") { @@ -68,85 +63,6 @@ export function getPackageVersion(): String { return version } -/* - Warning: Don't make any async calls in the loop body when using this function, since this may cause - funky concurrency behavior. - */ -export async function* scanDirectory(path: string, opts?: klaw.Options): AsyncIterableIterator { - let done = false - let resolver - let rejecter - - klaw(path, opts) - .on("data", (item) => { - if (item.path !== path) { - resolver(item) - } - }) - .on("error", (err) => { - rejecter(err) - }) - .on("end", () => { - done = true - resolver() - }) - - // a nice little trick to turn the stream into an async generator - while (!done) { - const promise: Promise = new Promise((resolve, reject) => { - resolver = resolve - rejecter = reject - }) - - yield await promise - } -} - -export async function getChildDirNames(parentDir: string): Promise { - let dirNames: string[] = [] - // Filter on hidden dirs by default. We could make the filter function a param if needed later - const filter = (item: string) => !basename(item).startsWith(".") - - for await (const item of scanDirectory(parentDir, { depthLimit: 0, filter })) { - if (!item || !item.stats.isDirectory()) { - continue - } - dirNames.push(basename(item.path)) - } - return dirNames -} - -export interface Ignorer { - ignores: (path: string) => boolean -} - -export async function getIgnorer(rootPath: string): Promise { - // TODO: this doesn't handle nested .gitignore files, we should revisit - const gitignorePath = join(rootPath, ".gitignore") - const gardenignorePath = join(rootPath, ".gardenignore") - const ig = ignore() - - if (await pathExists(gitignorePath)) { - ig.add((await readFile(gitignorePath)).toString()) - } - - if (await pathExists(gardenignorePath)) { - ig.add((await readFile(gardenignorePath)).toString()) - } - - // should we be adding this (or more) by default? - ig.add([ - "node_modules", - ".git", - "*.log", - GARDEN_DIR_NAME, - // TODO Take a better look at the temp files mutagen creates - ".mutagen-*", - ]) - - return ig -} - export async function sleep(msec) { return new Promise(resolve => setTimeout(resolve, msec)) } @@ -430,19 +346,6 @@ export function uniqByName(array: T[]): T[] { return uniqBy(array, item => item.name) } -/** - * Converts a Windows-style path to a cygwin style path (e.g. C:\some\folder -> /cygdrive/c/some/folder). - */ -export function toCygwinPath(path: string) { - const parsed = win32.parse(path) - const drive = parsed.root.split(":")[0].toLowerCase() - const dirs = parsed.dir.split(win32.sep).slice(1) - const cygpath = posix.join("/cygdrive", drive, ...dirs, parsed.base) - - // make sure trailing slash is retained - return path.endsWith(win32.sep) ? cygpath + posix.sep : cygpath -} - /** * Converts a string identifier to the appropriate casing and style for use in environment variable names. * (e.g. "my-service" -> "MY_SERVICE") diff --git a/garden-service/src/vcs/git.ts b/garden-service/src/vcs/git.ts index e47ddb710a..cb75149b6f 100644 --- a/garden-service/src/vcs/git.ts +++ b/garden-service/src/vcs/git.ts @@ -10,14 +10,16 @@ import execa = require("execa") import { join, resolve } from "path" import { ensureDir, pathExists } from "fs-extra" -import { NEW_MODULE_VERSION, VcsHandler, RemoteSourceParams } from "./base" +import { VcsHandler, RemoteSourceParams } from "./vcs" import { ConfigurationError, RuntimeError } from "../exceptions" +import * as Bluebird from "bluebird" +import { matchGlobs } from "../util/fs" -export function getCommitIdFromRefList(refList: string): string { +export function getCommitIdFromRefList(refList: string[]): string { try { - return refList.split("\n")[0].split("\t")[0] + return refList[0].split("\t")[0] } catch (err) { - return refList + return refList[0] } } @@ -34,63 +36,74 @@ export function parseGitUrl(url: string) { return parsed } +interface GitCli { + (...args: string[]): Promise +} + // TODO Consider moving git commands to separate (and testable) functions export class GitHandler extends VcsHandler { name = "git" - private gitCli(cwd: string) { + private gitCli(cwd: string): GitCli { return async (...args: string[]) => { - return execa.stdout("git", args, { cwd }) + const output = await execa.stdout("git", args, { cwd }) + return output.split("\n").filter(line => line.length > 0) } } - async getLatestCommit(path: string) { - const git = this.gitCli(path) - + private async getModifiedFiles(git: GitCli, path: string) { try { - return await git( - "rev-list", - "--max-count=1", - "--abbrev-commit", - "--abbrev=10", - "HEAD", - ) || NEW_MODULE_VERSION + return await git("diff-index", "--name-only", "HEAD", path) } catch (err) { if (err.code === 128) { - // not in a repo root, use default version - return NEW_MODULE_VERSION + // no commit in repo + return [] } else { throw err } } } - async getDirtyFiles(path: string) { + async getFiles(path: string, include?: string[]) { const git = this.gitCli(path) - let modifiedFiles: string[] - const repoRoot = await git("rev-parse", "--show-toplevel") + let lines: string[] = [] + let ignored: string[] = [] try { - modifiedFiles = (await git("diff-index", "--name-only", "HEAD", path)) - .split("\n") - .filter((f) => f.length > 0) - .map(file => resolve(repoRoot, file)) + lines = await git("ls-files", "-s", "--other", path) + ignored = await git("ls-files", "--ignored", "--exclude-per-directory=.gardenignore", path) } catch (err) { - if (err.code === 128) { - // no commit in repo - modifiedFiles = [] - } else { + // if we get 128 we're not in a repo root, so we get no files + if (err.code !== 128) { throw err } } - const newFiles = (await git("ls-files", "--other", "--exclude-standard", path)) - .split("\n") - .filter((f) => f.length > 0) - .map(file => resolve(path, file)) - - return modifiedFiles.concat(newFiles) + const files = await Bluebird.map(lines, async (line) => { + const split = line.trim().split(" ") + if (split.length === 1) { + // File is untracked + return { path: split[0] } + } else { + return { path: split[2].split("\t")[1], hash: split[1] } + } + }) + + const modified = new Set(await this.getModifiedFiles(git, path)) + const filtered = files + .filter(f => !include || matchGlobs(f.path, include)) + .filter(f => !ignored.includes(f.path)) + + return Bluebird.map(filtered, async (f) => { + const resolvedPath = resolve(path, f.path) + if (!f.hash || modified.has(f.path)) { + const hash = (await git("hash-object", resolvedPath))[0] + return { path: resolvedPath, hash } + } else { + return { path: resolvedPath, hash: f.hash } + } + }) } // TODO Better auth handling diff --git a/garden-service/src/vcs/base.ts b/garden-service/src/vcs/vcs.ts similarity index 65% rename from garden-service/src/vcs/base.ts rename to garden-service/src/vcs/vcs.ts index ccbaa28eef..8a1ac0df40 100644 --- a/garden-service/src/vcs/base.ts +++ b/garden-service/src/vcs/vcs.ts @@ -7,35 +7,31 @@ */ import * as Bluebird from "bluebird" -import { mapValues, keyBy, last, sortBy, omit } from "lodash" +import { mapValues, keyBy, sortBy, omit } from "lodash" import { createHash } from "crypto" import * as Joi from "joi" -import { validate } from "../config/common" +import { validate, joiArray } from "../config/common" import { join } from "path" import { GARDEN_VERSIONFILE_NAME } from "../constants" -import { pathExists, readFile, writeFile, stat } from "fs-extra" +import { pathExists, readFile, writeFile } from "fs-extra" import { ConfigurationError } from "../exceptions" -import { - ExternalSourceType, - getRemoteSourcesDirname, - getRemoteSourcePath, -} from "../util/ext-source-util" +import { ExternalSourceType, getRemoteSourcesDirname, getRemoteSourcePath } from "../util/ext-source-util" import { ModuleConfig, serializeConfig } from "../config/module" import { LogNode } from "../logger/log-node" export const NEW_MODULE_VERSION = "0000000000" export interface TreeVersion { - latestCommit: string - dirtyTimestamp: number | null + contentHash: string + files: string[] } export interface TreeVersions { [moduleName: string]: TreeVersion } export interface ModuleVersion { versionString: string - dirtyTimestamp: number | null dependencyVersions: TreeVersions + files: string[] } interface NamedTreeVersion extends TreeVersion { @@ -47,29 +43,25 @@ const versionStringSchema = Joi.string() .required() .description("String representation of the module version.") -const dirtyTimestampSchema = Joi.number() - .allow(null) - .required() - .description( - "Set to the last modified time (as UNIX timestamp) if the module contains uncommitted changes, otherwise null.", - ) +const fileNamesSchema = joiArray(Joi.string()) + .description("List of file paths included in the version.") export const treeVersionSchema = Joi.object() .keys({ - latestCommit: Joi.string() + contentHash: Joi.string() .required() - .description("The latest commit hash of the module source."), - dirtyTimestamp: dirtyTimestampSchema, + .description("The hash of all files in the directory, after filtering."), + files: fileNamesSchema, }) export const moduleVersionSchema = Joi.object() .keys({ versionString: versionStringSchema, - dirtyTimestamp: dirtyTimestampSchema, dependencyVersions: Joi.object() .pattern(/.+/, treeVersionSchema) .default(() => ({}), "{}") .description("The version of each of the dependencies of the module."), + files: fileNamesSchema, }) export interface RemoteSourceParams { @@ -79,49 +71,35 @@ export interface RemoteSourceParams { log: LogNode, } +export interface VcsFile { + path: string + hash: string +} + export abstract class VcsHandler { constructor(protected projectRoot: string) { } abstract name: string - abstract async getLatestCommit(path: string): Promise - abstract async getDirtyFiles(path: string): Promise + abstract async getFiles(path: string, include?: string[]): Promise abstract async ensureRemoteSource(params: RemoteSourceParams): Promise abstract async updateRemoteSource(params: RemoteSourceParams): Promise - async getTreeVersion(path: string) { - const commitHash = await this.getLatestCommit(path) - const dirtyFiles = await this.getDirtyFiles(path) - - let latestDirty = 0 - - // for dirty trees, we append the last modified time of last modified or added file - if (dirtyFiles.length) { - const stats = await Bluebird.filter(dirtyFiles, (file: string) => pathExists(file)) - .map((file: string) => stat(file)) - - let mtimes = stats.map((s) => Math.round(s.mtime.getTime() / 1000)) - let latest = mtimes.sort().slice(-1)[0] - - if (latest > latestDirty) { - latestDirty = latest - } - } - - return { - latestCommit: commitHash, - dirtyTimestamp: latestDirty || null, - } + // Note: explicitly requiring the include variable or null, to make sure it's specified + async getTreeVersion(path: string, include: string[] | null): Promise { + const files = await this.getFiles(path, include || undefined) + const contentHash = files.length > 0 ? hashFileHashes(files.map(f => f.hash)) : NEW_MODULE_VERSION + return { contentHash, files: files.map(f => f.path) } } - async resolveTreeVersion(path: string): Promise { + async resolveTreeVersion(path: string, include: string[] | null): Promise { // the version file is used internally to specify versions outside of source control const versionFilePath = join(path, GARDEN_VERSIONFILE_NAME) const fileVersion = await readTreeVersionFile(versionFilePath) - return fileVersion || this.getTreeVersion(path) + return fileVersion || this.getTreeVersion(path, include) } async resolveVersion(moduleConfig: ModuleConfig, dependencies: ModuleConfig[]): Promise { - const treeVersion = await this.resolveTreeVersion(moduleConfig.path) + const treeVersion = await this.resolveTreeVersion(moduleConfig.path, moduleConfig.include || null) validate(treeVersion, treeVersionSchema, { context: `${this.name} tree version for module at ${moduleConfig.path}`, @@ -131,29 +109,28 @@ export abstract class VcsHandler { const versionString = getVersionString( moduleConfig, [{ ...treeVersion, name: moduleConfig.name }], - treeVersion.dirtyTimestamp) + ) return { versionString, - dirtyTimestamp: treeVersion.dirtyTimestamp, dependencyVersions: {}, + files: treeVersion.files, } } const namedDependencyVersions = await Bluebird.map( dependencies, - async (m: ModuleConfig) => ({ name: m.name, ...await this.resolveTreeVersion(m.path) }), + async (m: ModuleConfig) => ({ name: m.name, ...await this.resolveTreeVersion(m.path, m.include || null) }), ) const dependencyVersions = mapValues(keyBy(namedDependencyVersions, "name"), v => omit(v, "name")) // keep the module at the top of the chain, dependencies sorted by name const allVersions: NamedTreeVersion[] = [{ name: moduleConfig.name, ...treeVersion }] .concat(namedDependencyVersions) - const dirtyTimestamp = getLatestDirty(allVersions) return { - dirtyTimestamp, dependencyVersions, - versionString: getVersionString(moduleConfig, allVersions, dirtyTimestamp), + versionString: getVersionString(moduleConfig, allVersions), + files: treeVersion.files, } } @@ -214,31 +191,23 @@ export async function writeModuleVersionFile(path: string, version: ModuleVersio * when the version string is used in template variables in configuration files. */ export function getVersionString( - moduleConfig: ModuleConfig, treeVersions: NamedTreeVersion[], dirtyTimestamp: number | null, + moduleConfig: ModuleConfig, treeVersions: NamedTreeVersion[], ) { - const hashed = `v-${hashVersions(moduleConfig, treeVersions)}` - return dirtyTimestamp ? `${hashed}-${dirtyTimestamp}` : hashed -} - -/** - * Returns the latest (i.e. numerically largest) dirty timestamp found in versions, or null if none of versions - * has a dirty timestamp. - */ -export function getLatestDirty(versions: TreeVersion[]): number | null { - const latest = last(sortBy( - versions.filter(v => !!v.dirtyTimestamp), v => v.dirtyTimestamp) - .map(v => v.dirtyTimestamp)) - return latest || null + return `v-${hashVersions(moduleConfig, treeVersions)}` } /** * The versions argument should consist of moduleConfig's tree version, and the tree versions of its dependencies. */ export function hashVersions(moduleConfig: ModuleConfig, versions: NamedTreeVersion[]) { - const versionHash = createHash("sha256") const configString = serializeConfig(moduleConfig) const versionStrings = sortBy(versions, "name") - .map(v => `${v.name}_${v.latestCommit}`) - versionHash.update([configString, ...versionStrings].join(".")) + .map(v => `${v.name}_${v.contentHash}`) + return hashFileHashes([configString, ...versionStrings]) +} + +export function hashFileHashes(hashes: string[]) { + const versionHash = createHash("sha256") + versionHash.update(hashes.join(".")) return versionHash.digest("hex").slice(0, 10) } diff --git a/garden-service/src/watch.ts b/garden-service/src/watch.ts index 4a4ba74f51..e9a219009b 100644 --- a/garden-service/src/watch.ts +++ b/garden-service/src/watch.ts @@ -10,11 +10,13 @@ import { watch, FSWatcher } from "chokidar" import { parse, relative } from "path" import { pathToCacheContext } from "./cache" import { Module } from "./types/module" -import { MODULE_CONFIG_FILENAME } from "./constants" +import { CONFIG_FILENAME } from "./constants" import { Garden } from "./garden" import { LogEntry } from "./logger/log-entry" import * as klaw from "klaw" import { registerCleanupFunction } from "./util/util" +import * as Bluebird from "bluebird" +import { some } from "lodash" export type ChangeHandler = (module: Module | null, configChanged: boolean) => Promise @@ -54,7 +56,7 @@ export class Watcher { }) this.watcher - .on("add", this.makeFileChangedHandler("added", modules)) + .on("add", this.makeFileAddedHandler(modules)) .on("change", this.makeFileChangedHandler("modified", modules)) .on("unlink", this.makeFileChangedHandler("removed", modules)) .on("addDir", this.makeDirAddedHandler(modules)) @@ -74,41 +76,70 @@ export class Watcher { } } + private makeFileAddedHandler(modules: Module[]) { + return this.wrapAsync(async (path: string) => { + this.log.debug(`Watcher: File ${path} added`) + + const changedModules = await Bluebird.filter(modules, async (m) => { + const files = await this.garden.vcs.getFiles(m.path) + return some(files, f => f.path === path) + }) + + this.sourcesChanged(modules, changedModules, path, "added") + }) + } + + private wrapAsync(listener: (path: string) => Promise) { + const _this = this + + return (path: string) => { + listener(path) + .catch(err => { + console.log("catch", err) + _this.watcher.emit("error", err) + }) + } + } + private makeFileChangedHandler(type: string, modules: Module[]) { return (path: string) => { this.log.debug(`Watcher: File ${path} ${type}`) - const parsed = parse(path) - const filename = parsed.base + const changedModules = modules.filter(m => m.version.files.includes(path)) - // changedModules will only have more than one element when the changed path belongs to >= 2 modules. - const changedModules = modules.filter(m => path.startsWith(m.path)) - const changedModuleNames = changedModules.map(m => m.name) + this.sourcesChanged(modules, changedModules, path, type) + } + } + + private sourcesChanged(modules: Module[], changedModules: Module[], path: string, type: string) { + const changedModuleNames = changedModules.map(m => m.name) - if (filename === MODULE_CONFIG_FILENAME || filename === ".gitignore" || filename === ".gardenignore") { - this.invalidateCached(modules) + const parsed = parse(path) + const filename = parsed.base - if (changedModuleNames.length > 0) { - this.garden.events.emit("moduleConfigChanged", { names: changedModuleNames, path }) - } else if (filename === MODULE_CONFIG_FILENAME) { - if (parsed.dir === this.garden.projectRoot) { - this.garden.events.emit("projectConfigChanged", {}) + if (filename === CONFIG_FILENAME || filename === ".gitignore" || filename === ".gardenignore") { + this.invalidateCached(modules) + + if (changedModuleNames.length > 0) { + this.garden.events.emit("moduleConfigChanged", { names: changedModuleNames, path }) + } else if (filename === CONFIG_FILENAME) { + if (parsed.dir === this.garden.projectRoot) { + this.garden.events.emit("projectConfigChanged", {}) + } else { + if (type === "added") { + this.garden.events.emit("configAdded", { path }) } else { - if (type === "added") { - this.garden.events.emit("configAdded", { path }) - } else { - this.garden.events.emit("configRemoved", { path }) - } + this.garden.events.emit("configRemoved", { path }) } } - - return } - if (changedModuleNames.length > 0) { - this.invalidateCached(changedModules) - this.garden.events.emit("moduleSourcesChanged", { names: changedModuleNames, pathChanged: path }) - } + return + } + + if (changedModuleNames.length > 0) { + this.invalidateCached(changedModules) + this.garden.events.emit("moduleSourcesChanged", { names: changedModuleNames, pathChanged: path }) } } @@ -129,7 +160,7 @@ export class Watcher { klaw(path, scanOpts) .on("data", (item) => { const parsed = parse(item.path) - if (item.path !== path && parsed.base === MODULE_CONFIG_FILENAME) { + if (item.path !== path && parsed.base === CONFIG_FILENAME) { configChanged = true this.garden.events.emit("configAdded", { path: item.path }) } diff --git a/garden-service/test/helpers.ts b/garden-service/test/helpers.ts index 92db15aa96..e9753bdf48 100644 --- a/garden-service/test/helpers.ts +++ b/garden-service/test/helpers.ts @@ -33,10 +33,11 @@ import { RunTaskParams, SetSecretParams, } from "../src/types/plugin/params" -import { ModuleVersion } from "../src/vcs/base" -import { GARDEN_DIR_NAME } from "../src/constants" +import { ModuleVersion } from "../src/vcs/vcs" +import { GARDEN_DIR_NAME, CONFIG_FILENAME } from "../src/constants" import { EventBus, Events } from "../src/events" -import { ValueOf, Ignorer } from "../src/util/util" +import { ValueOf } from "../src/util/util" +import { Ignorer } from "../src/util/fs" import { SourceConfig } from "../src/config/project" import { BuildDir } from "../src/build-dir" import timekeeper = require("timekeeper") @@ -47,12 +48,12 @@ export const testNow = new Date() export const testModuleVersionString = "v-1234512345" export const testModuleVersion: ModuleVersion = { versionString: testModuleVersionString, - dirtyTimestamp: null, dependencyVersions: {}, + files: [], } -export function getDataDir(name: string) { - return resolve(dataDir, name) +export function getDataDir(...names: string[]) { + return resolve(dataDir, ...names) } export async function profileBlock(description: string, block: () => Promise) { @@ -381,7 +382,7 @@ export function stubExtSources(garden: Garden) { } export function getExampleProjects() { - const names = readdirSync(examplesDir).filter(n => existsSync(join(examplesDir, n, "garden.yml"))) + const names = readdirSync(examplesDir).filter(n => existsSync(join(examplesDir, n, CONFIG_FILENAME))) return fromPairs(names.map(n => [n, join(examplesDir, n)])) } diff --git a/garden-service/test/integ/src/integ-helpers.ts b/garden-service/test/integ/src/integ-helpers.ts index 3871f534c5..ea00b97c6e 100644 --- a/garden-service/test/integ/src/integ-helpers.ts +++ b/garden-service/test/integ/src/integ-helpers.ts @@ -14,7 +14,7 @@ describe("integ-helpers", () => { describe("findTasks", () => { before(async () => { - testEntries = await runGarden("/Users/ths/go/src/garden/examples/vote", ["test"]) + testEntries = await runGarden(voteExamplePath, ["test"]) }) const specs = [ diff --git a/garden-service/test/run-garden.ts b/garden-service/test/run-garden.ts index 605165e744..976620c10d 100644 --- a/garden-service/test/run-garden.ts +++ b/garden-service/test/run-garden.ts @@ -77,7 +77,7 @@ export function commandReloadedStep(): WatchTestStep { * The GardenWatch class below, on the other hand, is for running/testing watch-mode commands. */ export async function runGarden(dir: string, command: string[]): Promise { - const out = (await execa(gardenBinPath, [...command, "--loggerType", "json", "-l", "4"], { cwd: dir })).stdout + const out = (await execa(gardenBinPath, [...command, "--logger-type", "json", "-l", "4"], { cwd: dir })).stdout return parseLogEntries(out.split("\n").filter(Boolean)) } @@ -155,7 +155,7 @@ export class GardenWatch { this.currentTestStepIdx = 0 this.testSteps = testSteps - this.proc = execa(gardenBinPath, [...this.command, "--loggerType", "json", "-l", "4"], { cwd: this.dir }) + this.proc = execa(gardenBinPath, [...this.command, "--logger-type", "json", "-l", "4"], { cwd: this.dir }) this.proc.stdout.on("data", (rawLine) => { const lines = rawLine.toString().trim().split("\n") if (showLog) { diff --git a/garden-service/test/unit/data/test-project-a/module-a/.garden-version b/garden-service/test/unit/data/test-project-a/module-a/.garden-version index 1d754b7141..eb45f3a14b 100644 --- a/garden-service/test/unit/data/test-project-a/module-a/.garden-version +++ b/garden-service/test/unit/data/test-project-a/module-a/.garden-version @@ -1,4 +1,3 @@ { - "latestCommit": "1234567890", - "dirtyTimestamp": null + "contentHash": "1234567890" } diff --git a/garden-service/test/unit/data/test-project-duplicate-project-config/module-a/.garden-version b/garden-service/test/unit/data/test-project-duplicate-project-config/module-a/.garden-version index 1d754b7141..eb45f3a14b 100644 --- a/garden-service/test/unit/data/test-project-duplicate-project-config/module-a/.garden-version +++ b/garden-service/test/unit/data/test-project-duplicate-project-config/module-a/.garden-version @@ -1,4 +1,3 @@ { - "latestCommit": "1234567890", - "dirtyTimestamp": null + "contentHash": "1234567890" } diff --git a/garden-service/test/unit/data/test-project-exec/module-a/.garden-version b/garden-service/test/unit/data/test-project-exec/module-a/.garden-version index 1d754b7141..eb45f3a14b 100644 --- a/garden-service/test/unit/data/test-project-exec/module-a/.garden-version +++ b/garden-service/test/unit/data/test-project-exec/module-a/.garden-version @@ -1,4 +1,3 @@ { - "latestCommit": "1234567890", - "dirtyTimestamp": null + "contentHash": "1234567890" } diff --git a/garden-service/test/unit/data/test-project-ext-module-sources/mock-dot-garden/sources/module/module-a/.garden-version b/garden-service/test/unit/data/test-project-ext-module-sources/mock-dot-garden/sources/module/module-a/.garden-version index 1d754b7141..eb45f3a14b 100644 --- a/garden-service/test/unit/data/test-project-ext-module-sources/mock-dot-garden/sources/module/module-a/.garden-version +++ b/garden-service/test/unit/data/test-project-ext-module-sources/mock-dot-garden/sources/module/module-a/.garden-version @@ -1,4 +1,3 @@ { - "latestCommit": "1234567890", - "dirtyTimestamp": null + "contentHash": "1234567890" } diff --git a/garden-service/test/unit/data/test-project-ext-module-sources/mock-dot-garden/sources/module/module-c/.garden-version b/garden-service/test/unit/data/test-project-ext-module-sources/mock-dot-garden/sources/module/module-c/.garden-version index 1d754b7141..eb45f3a14b 100644 --- a/garden-service/test/unit/data/test-project-ext-module-sources/mock-dot-garden/sources/module/module-c/.garden-version +++ b/garden-service/test/unit/data/test-project-ext-module-sources/mock-dot-garden/sources/module/module-c/.garden-version @@ -1,4 +1,3 @@ { - "latestCommit": "1234567890", - "dirtyTimestamp": null + "contentHash": "1234567890" } diff --git a/garden-service/test/unit/data/test-project-ext-module-sources/mock-local-path/module-a/.garden-version b/garden-service/test/unit/data/test-project-ext-module-sources/mock-local-path/module-a/.garden-version index 1d754b7141..eb45f3a14b 100644 --- a/garden-service/test/unit/data/test-project-ext-module-sources/mock-local-path/module-a/.garden-version +++ b/garden-service/test/unit/data/test-project-ext-module-sources/mock-local-path/module-a/.garden-version @@ -1,4 +1,3 @@ { - "latestCommit": "1234567890", - "dirtyTimestamp": null + "contentHash": "1234567890" } diff --git a/garden-service/test/unit/data/test-project-ext-module-sources/mock-local-path/module-c/.garden-version b/garden-service/test/unit/data/test-project-ext-module-sources/mock-local-path/module-c/.garden-version index 1d754b7141..eb45f3a14b 100644 --- a/garden-service/test/unit/data/test-project-ext-module-sources/mock-local-path/module-c/.garden-version +++ b/garden-service/test/unit/data/test-project-ext-module-sources/mock-local-path/module-c/.garden-version @@ -1,4 +1,3 @@ { - "latestCommit": "1234567890", - "dirtyTimestamp": null + "contentHash": "1234567890" } diff --git a/garden-service/test/unit/data/test-project-ext-module-sources/module-a/.garden-version b/garden-service/test/unit/data/test-project-ext-module-sources/module-a/.garden-version index 1d754b7141..eb45f3a14b 100644 --- a/garden-service/test/unit/data/test-project-ext-module-sources/module-a/.garden-version +++ b/garden-service/test/unit/data/test-project-ext-module-sources/module-a/.garden-version @@ -1,4 +1,3 @@ { - "latestCommit": "1234567890", - "dirtyTimestamp": null + "contentHash": "1234567890" } diff --git a/garden-service/test/unit/data/test-project-ext-module-sources/module-b/.garden-version b/garden-service/test/unit/data/test-project-ext-module-sources/module-b/.garden-version index 1d754b7141..eb45f3a14b 100644 --- a/garden-service/test/unit/data/test-project-ext-module-sources/module-b/.garden-version +++ b/garden-service/test/unit/data/test-project-ext-module-sources/module-b/.garden-version @@ -1,4 +1,3 @@ { - "latestCommit": "1234567890", - "dirtyTimestamp": null + "contentHash": "1234567890" } diff --git a/garden-service/test/unit/data/test-project-ext-module-sources/module-c/.garden-version b/garden-service/test/unit/data/test-project-ext-module-sources/module-c/.garden-version index 1d754b7141..eb45f3a14b 100644 --- a/garden-service/test/unit/data/test-project-ext-module-sources/module-c/.garden-version +++ b/garden-service/test/unit/data/test-project-ext-module-sources/module-c/.garden-version @@ -1,4 +1,3 @@ { - "latestCommit": "1234567890", - "dirtyTimestamp": null + "contentHash": "1234567890" } diff --git a/garden-service/test/unit/data/test-project-ext-project-sources/mock-dot-garden/sources/project/source-a/module-a/.garden-version b/garden-service/test/unit/data/test-project-ext-project-sources/mock-dot-garden/sources/project/source-a/module-a/.garden-version index 1d754b7141..eb45f3a14b 100644 --- a/garden-service/test/unit/data/test-project-ext-project-sources/mock-dot-garden/sources/project/source-a/module-a/.garden-version +++ b/garden-service/test/unit/data/test-project-ext-project-sources/mock-dot-garden/sources/project/source-a/module-a/.garden-version @@ -1,4 +1,3 @@ { - "latestCommit": "1234567890", - "dirtyTimestamp": null + "contentHash": "1234567890" } diff --git a/garden-service/test/unit/data/test-project-ext-project-sources/mock-local-path/source-a/module-a/.garden-version b/garden-service/test/unit/data/test-project-ext-project-sources/mock-local-path/source-a/module-a/.garden-version index 1d754b7141..eb45f3a14b 100644 --- a/garden-service/test/unit/data/test-project-ext-project-sources/mock-local-path/source-a/module-a/.garden-version +++ b/garden-service/test/unit/data/test-project-ext-project-sources/mock-local-path/source-a/module-a/.garden-version @@ -1,4 +1,3 @@ { - "latestCommit": "1234567890", - "dirtyTimestamp": null + "contentHash": "1234567890" } diff --git a/garden-service/test/unit/data/test-project-flat-config/invalid-config-kind/.garden-version b/garden-service/test/unit/data/test-project-flat-config/invalid-config-kind/.garden-version index 1d754b7141..eb45f3a14b 100644 --- a/garden-service/test/unit/data/test-project-flat-config/invalid-config-kind/.garden-version +++ b/garden-service/test/unit/data/test-project-flat-config/invalid-config-kind/.garden-version @@ -1,4 +1,3 @@ { - "latestCommit": "1234567890", - "dirtyTimestamp": null + "contentHash": "1234567890" } diff --git a/garden-service/test/unit/data/test-project-multiple-module-config/module-a/.garden-version b/garden-service/test/unit/data/test-project-multiple-module-config/module-a/.garden-version index 1d754b7141..eb45f3a14b 100644 --- a/garden-service/test/unit/data/test-project-multiple-module-config/module-a/.garden-version +++ b/garden-service/test/unit/data/test-project-multiple-module-config/module-a/.garden-version @@ -1,4 +1,3 @@ { - "latestCommit": "1234567890", - "dirtyTimestamp": null + "contentHash": "1234567890" } diff --git a/garden-service/test/unit/data/test-project-watch/.gardenignore b/garden-service/test/unit/data/test-project-watch/.gardenignore new file mode 100644 index 0000000000..418f6c60a9 --- /dev/null +++ b/garden-service/test/unit/data/test-project-watch/.gardenignore @@ -0,0 +1 @@ +**/project-excluded.txt \ No newline at end of file diff --git a/garden-service/test/unit/data/test-project-watch/module-a/.gardenignore b/garden-service/test/unit/data/test-project-watch/module-a/.gardenignore new file mode 100644 index 0000000000..b6a0dee0ea --- /dev/null +++ b/garden-service/test/unit/data/test-project-watch/module-a/.gardenignore @@ -0,0 +1 @@ +module-excluded.txt \ No newline at end of file diff --git a/garden-service/test/unit/data/test-project-watch/module-a/module-excluded.txt b/garden-service/test/unit/data/test-project-watch/module-a/module-excluded.txt new file mode 100644 index 0000000000..fc21ffd88d --- /dev/null +++ b/garden-service/test/unit/data/test-project-watch/module-a/module-excluded.txt @@ -0,0 +1 @@ +Nope! \ No newline at end of file diff --git a/garden-service/test/unit/data/test-project-watch/module-a/project-excluded.txt b/garden-service/test/unit/data/test-project-watch/module-a/project-excluded.txt new file mode 100644 index 0000000000..fc21ffd88d --- /dev/null +++ b/garden-service/test/unit/data/test-project-watch/module-a/project-excluded.txt @@ -0,0 +1 @@ +Nope! \ No newline at end of file diff --git a/garden-service/test/unit/data/test-project-watch/with-include/foo.txt b/garden-service/test/unit/data/test-project-watch/with-include/foo.txt new file mode 100644 index 0000000000..ba0e162e1c --- /dev/null +++ b/garden-service/test/unit/data/test-project-watch/with-include/foo.txt @@ -0,0 +1 @@ +bar \ No newline at end of file diff --git a/garden-service/test/unit/data/test-project-watch/with-include/garden.yml b/garden-service/test/unit/data/test-project-watch/with-include/garden.yml new file mode 100644 index 0000000000..0711d3876c --- /dev/null +++ b/garden-service/test/unit/data/test-project-watch/with-include/garden.yml @@ -0,0 +1,4 @@ +module: + name: with-include + type: test + include: ["subdir/foo2.txt"] diff --git a/garden-service/test/unit/data/test-project-watch/with-include/subdir/foo2.txt b/garden-service/test/unit/data/test-project-watch/with-include/subdir/foo2.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/garden-service/test/unit/data/test-projects/include-field/garden.yml b/garden-service/test/unit/data/test-projects/include-field/garden.yml new file mode 100644 index 0000000000..31f3b94ec2 --- /dev/null +++ b/garden-service/test/unit/data/test-projects/include-field/garden.yml @@ -0,0 +1,6 @@ +project: + name: include-filter + environments: + - name: local + providers: + - name: test-plugin diff --git a/garden-service/test/unit/data/test-projects/include-field/module-a/garden.yml b/garden-service/test/unit/data/test-projects/include-field/module-a/garden.yml new file mode 100644 index 0000000000..506273076e --- /dev/null +++ b/garden-service/test/unit/data/test-projects/include-field/module-a/garden.yml @@ -0,0 +1,4 @@ +kind: Module +name: module-a +type: test +include: ["yes.txt"] diff --git a/garden-service/test/unit/data/test-projects/include-field/module-a/nope.txt b/garden-service/test/unit/data/test-projects/include-field/module-a/nope.txt new file mode 100644 index 0000000000..c5cfed716a --- /dev/null +++ b/garden-service/test/unit/data/test-projects/include-field/module-a/nope.txt @@ -0,0 +1 @@ +Go away! \ No newline at end of file diff --git a/garden-service/test/unit/data/test-projects/include-field/module-a/yes.txt b/garden-service/test/unit/data/test-projects/include-field/module-a/yes.txt new file mode 100644 index 0000000000..80cccdde3b --- /dev/null +++ b/garden-service/test/unit/data/test-projects/include-field/module-a/yes.txt @@ -0,0 +1 @@ +Oh hai! \ No newline at end of file diff --git a/garden-service/test/unit/data/test-projects/missing-deps/missing-build-dep/module-a/.garden-version b/garden-service/test/unit/data/test-projects/missing-deps/missing-build-dep/module-a/.garden-version index 1d754b7141..eb45f3a14b 100644 --- a/garden-service/test/unit/data/test-projects/missing-deps/missing-build-dep/module-a/.garden-version +++ b/garden-service/test/unit/data/test-projects/missing-deps/missing-build-dep/module-a/.garden-version @@ -1,4 +1,3 @@ { - "latestCommit": "1234567890", - "dirtyTimestamp": null + "contentHash": "1234567890" } diff --git a/garden-service/test/unit/data/test-projects/missing-deps/missing-runtime-dep/module-a/.garden-version b/garden-service/test/unit/data/test-projects/missing-deps/missing-runtime-dep/module-a/.garden-version index 1d754b7141..eb45f3a14b 100644 --- a/garden-service/test/unit/data/test-projects/missing-deps/missing-runtime-dep/module-a/.garden-version +++ b/garden-service/test/unit/data/test-projects/missing-deps/missing-runtime-dep/module-a/.garden-version @@ -1,4 +1,3 @@ { - "latestCommit": "1234567890", - "dirtyTimestamp": null + "contentHash": "1234567890" } diff --git a/garden-service/test/unit/src/build-dir.ts b/garden-service/test/unit/src/build-dir.ts index cc503d3a71..a4baa68737 100644 --- a/garden-service/test/unit/src/build-dir.ts +++ b/garden-service/test/unit/src/build-dir.ts @@ -4,6 +4,7 @@ import { pathExists, readdir } from "fs-extra" import { expect } from "chai" import { BuildTask } from "../../../src/tasks/build" import { makeTestGarden } from "../../helpers" +import { CONFIG_FILENAME } from "../../../src/constants" /* Module dependency diagram for test-project-build-products @@ -51,7 +52,7 @@ describe("BuildDir", () => { const buildDirA = await garden.buildDir.buildPath(moduleA.name) const copiedPaths = [ - join(buildDirA, "garden.yml"), + join(buildDirA, CONFIG_FILENAME), join(buildDirA, "some-dir", "some-file"), ] diff --git a/garden-service/test/unit/src/commands/deploy.ts b/garden-service/test/unit/src/commands/deploy.ts index dcb6f07cca..b07ff56bef 100644 --- a/garden-service/test/unit/src/commands/deploy.ts +++ b/garden-service/test/unit/src/commands/deploy.ts @@ -21,7 +21,7 @@ const placeholderTaskResult = (moduleName, taskName, command) => ({ command, version: { versionString: "v-1", - dirtyTimestamp: null, + files: [], dependencyVersions: {}, }, success: true, diff --git a/garden-service/test/unit/src/commands/publish.ts b/garden-service/test/unit/src/commands/publish.ts index f9934a1221..3ddf7cc255 100644 --- a/garden-service/test/unit/src/commands/publish.ts +++ b/garden-service/test/unit/src/commands/publish.ts @@ -2,14 +2,11 @@ import chalk from "chalk" import { it } from "mocha" import { join } from "path" import { expect } from "chai" -import * as td from "testdouble" import { Garden } from "../../../../src/garden" import { PluginFactory } from "../../../../src/types/plugin/plugin" import { PublishCommand } from "../../../../src/commands/publish" import { makeTestGardenA, configureTestModule } from "../../../helpers" -import { expectError, taskResultOutputs } from "../../../helpers" -import { ModuleVersion } from "../../../../src/vcs/base" -import { LogEntry } from "../../../../src/logger/log-entry" +import { taskResultOutputs } from "../../../helpers" const projectRootB = join(__dirname, "..", "..", "data", "test-project-b") @@ -177,58 +174,4 @@ describe("PublishCommand", () => { }, }) }) - - context("module is dirty", () => { - let garden - let log: LogEntry - - beforeEach(async () => { - td.replace(Garden.prototype, "resolveVersion", async (): Promise => { - return { - versionString: "012345", - dirtyTimestamp: 12345, - dependencyVersions: {}, - } - }) - garden = await getTestGarden() - log = garden.log - }) - - it("should throw if module is dirty", async () => { - const command = new PublishCommand() - - await expectError(() => command.action({ - garden, - log, - args: { - modules: ["module-a"], - }, - opts: { - "allow-dirty": false, - "force-build": false, - }, - }), "runtime") - }) - - it("should optionally allow publishing dirty commits", async () => { - const command = new PublishCommand() - - const { result } = await command.action({ - garden, - log, - args: { - modules: ["module-a"], - }, - opts: { - "allow-dirty": true, - "force-build": true, - }, - }) - - expect(taskResultOutputs(result!)).to.eql({ - "build.module-a": { fresh: true }, - "publish.module-a": { published: true }, - }) - }) - }) }) diff --git a/garden-service/test/unit/src/config/base.ts b/garden-service/test/unit/src/config/base.ts index 5a7e8aeeaa..63d49ef9bf 100644 --- a/garden-service/test/unit/src/config/base.ts +++ b/garden-service/test/unit/src/config/base.ts @@ -80,6 +80,7 @@ describe("loadConfig", () => { name: "module-a", type: "test", description: undefined, + include: undefined, repositoryUrl: undefined, allowPublish: true, build: { dependencies: [] }, @@ -148,6 +149,7 @@ describe("loadConfig", () => { repositoryUrl: undefined, allowPublish: true, build: { dependencies: [] }, + include: undefined, outputs: {}, path: projectPathMultipleModules, serviceConfigs: [], @@ -172,6 +174,7 @@ describe("loadConfig", () => { type: "test", allowPublish: true, description: undefined, + include: undefined, repositoryUrl: undefined, build: { dependencies: [ @@ -201,6 +204,7 @@ describe("loadConfig", () => { type: "test", allowPublish: true, description: undefined, + include: undefined, repositoryUrl: undefined, build: { dependencies: [] }, outputs: {}, @@ -259,6 +263,7 @@ describe("loadConfig", () => { dependencies: [], }, allowPublish: true, + include: undefined, outputs: {}, path: projectPathFlat, repositoryUrl: undefined, diff --git a/garden-service/test/unit/src/garden.ts b/garden-service/test/unit/src/garden.ts index 442cbdb8d7..39a1c19664 100644 --- a/garden-service/test/unit/src/garden.ts +++ b/garden-service/test/unit/src/garden.ts @@ -17,7 +17,7 @@ import { import { getNames } from "../../../src/util/util" import { MOCK_CONFIG } from "../../../src/cli/cli" import { LinkedSource } from "../../../src/config-store" -import { ModuleVersion } from "../../../src/vcs/base" +import { ModuleVersion } from "../../../src/vcs/vcs" import { hashRepoUrl } from "../../../src/util/ext-source-util" import { getModuleCacheContext } from "../../../src/types/module" @@ -224,8 +224,8 @@ describe("Garden", () => { const module = await garden.resolveModuleConfig("module-a") const version: ModuleVersion = { versionString: "banana", - dirtyTimestamp: 987654321, dependencyVersions: {}, + files: [], } garden.cache.set(["moduleVersions", module.name], version, getModuleCacheContext(module)) @@ -243,8 +243,8 @@ describe("Garden", () => { const resolveStub = td.replace(garden.vcs, "resolveVersion") const version: ModuleVersion = { versionString: "banana", - dirtyTimestamp: 987654321, dependencyVersions: {}, + files: [], } td.when(resolveStub(), { ignoreExtraArgs: true }).thenResolve(version) @@ -259,8 +259,8 @@ describe("Garden", () => { const module = await garden.resolveModuleConfig("module-a") const version: ModuleVersion = { versionString: "banana", - dirtyTimestamp: 987654321, dependencyVersions: {}, + files: [], } garden.cache.set(["moduleVersions", module.name], version, getModuleCacheContext(module)) diff --git a/garden-service/test/unit/src/plugins/container.ts b/garden-service/test/unit/src/plugins/container.ts index a924f7acc5..d806c412a6 100644 --- a/garden-service/test/unit/src/plugins/container.ts +++ b/garden-service/test/unit/src/plugins/container.ts @@ -65,8 +65,8 @@ describe("plugins.container", () => { td.replace(Garden.prototype, "resolveVersion", async () => ({ versionString: "1234", - dirtyTimestamp: null, - dependencyVersions: [], + dependencyVersions: {}, + files: [], })) }) diff --git a/garden-service/test/unit/src/plugins/exec.ts b/garden-service/test/unit/src/plugins/exec.ts index 297d8d4cdd..68a82bd49d 100644 --- a/garden-service/test/unit/src/plugins/exec.ts +++ b/garden-service/test/unit/src/plugins/exec.ts @@ -9,7 +9,7 @@ import { ConfigGraph } from "../../../../src/config-graph" import { writeModuleVersionFile, readModuleVersionFile, -} from "../../../../src/vcs/base" +} from "../../../../src/vcs/vcs" import { dataDir, makeTestGarden, diff --git a/garden-service/test/unit/src/plugins/kubernetes/container/ingress.ts b/garden-service/test/unit/src/plugins/kubernetes/container/ingress.ts index c3d8ba35b4..08f9f439fa 100644 --- a/garden-service/test/unit/src/plugins/kubernetes/container/ingress.ts +++ b/garden-service/test/unit/src/plugins/kubernetes/container/ingress.ts @@ -313,8 +313,8 @@ describe("createIngressResources", () => { td.replace(Garden.prototype, "resolveVersion", async () => ({ versionString: "1234", - dirtyTimestamp: null, - dependencyVersions: [], + dependencyVersions: {}, + files: [], })) }) diff --git a/garden-service/test/unit/src/plugins/kubernetes/helm/config.ts b/garden-service/test/unit/src/plugins/kubernetes/helm/config.ts index 034dfe96cd..71b59823c7 100644 --- a/garden-service/test/unit/src/plugins/kubernetes/helm/config.ts +++ b/garden-service/test/unit/src/plugins/kubernetes/helm/config.ts @@ -46,6 +46,7 @@ describe("validateHelmModule", () => { }, description: "The API backend for the voting UI", apiVersion: "garden.io/v0", + include: undefined, name: "api", outputs: { "release-name": "api-release", diff --git a/garden-service/test/unit/src/task-graph.ts b/garden-service/test/unit/src/task-graph.ts index 54a8402146..6f4db08854 100644 --- a/garden-service/test/unit/src/task-graph.ts +++ b/garden-service/test/unit/src/task-graph.ts @@ -40,8 +40,8 @@ class TestTask extends BaseTask { log: garden.log, version: { versionString: "12345-6789", - dirtyTimestamp: 6789, dependencyVersions: {}, + files: [], }, force, }) diff --git a/garden-service/test/unit/src/tasks/test.ts b/garden-service/test/unit/src/tasks/test.ts index 147e92f9e0..87771b7b2d 100644 --- a/garden-service/test/unit/src/tasks/test.ts +++ b/garden-service/test/unit/src/tasks/test.ts @@ -6,6 +6,7 @@ import { Garden } from "../../../../src/garden" import { dataDir, makeTestGarden } from "../../../helpers" import { LogEntry } from "../../../../src/logger/log-entry" import { ConfigGraph } from "../../../../src/config-graph" +import { ModuleVersion } from "../../../../src/vcs/vcs" describe("TestTask", () => { let garden: Garden @@ -23,22 +24,32 @@ describe("TestTask", () => { const resolveVersion = td.replace(garden, "resolveVersion") - const version = { + const versionA: ModuleVersion = { versionString: "v6fb19922cd", - dirtyTimestamp: null, dependencyVersions: { "module-b": { - latestCommit: "abcdefg1234", - dirtyTimestamp: null, + contentHash: "abcdefg1234", + files: [], }, }, + files: [], } + const versionB: ModuleVersion = { + versionString: "abcdefg1234", + dependencyVersions: {}, + files: [], + } + + td.when(resolveVersion("module-a", [])).thenResolve(versionA) + td.when(resolveVersion("module-b", [])).thenResolve(versionB) + const moduleB = await graph.getModule("module-b") - td.when(resolveVersion("module-a", [moduleB])).thenResolve(version) + td.when(resolveVersion("module-a", [moduleB])).thenResolve(versionA) const moduleA = await graph.getModule("module-a") + const testConfig = moduleA.testConfigs[0] const task = await TestTask.factory({ @@ -51,6 +62,6 @@ describe("TestTask", () => { forceBuild: false, }) - expect(task.version).to.eql(version) + expect(task.version).to.eql(versionA) }) }) diff --git a/garden-service/test/unit/src/types/module.ts b/garden-service/test/unit/src/types/module.ts new file mode 100644 index 0000000000..b6766d5138 --- /dev/null +++ b/garden-service/test/unit/src/types/module.ts @@ -0,0 +1,13 @@ +import { expect } from "chai" +import { getDataDir, makeTestGarden } from "../../../helpers" + +describe("moduleFromConfig", () => { + it("should add module config file to version files if needed", async () => { + const projectRoot = await getDataDir("test-projects", "include-field") + const garden = await makeTestGarden(projectRoot) + const graph = await garden.getConfigGraph() + const module = await graph.getModule("module-a") + + expect(module.version.files).to.include(module.configPath) + }) +}) diff --git a/garden-service/test/unit/src/util/fs.ts b/garden-service/test/unit/src/util/fs.ts new file mode 100644 index 0000000000..80b41dd78e --- /dev/null +++ b/garden-service/test/unit/src/util/fs.ts @@ -0,0 +1,56 @@ +import { expect } from "chai" +import { join } from "path" +import { getDataDir } from "../../../helpers" +import { scanDirectory, toCygwinPath, getChildDirNames } from "../../../../src/util/fs" + +describe("util", () => { + describe("scanDirectory", () => { + it("should iterate through all files in a directory", async () => { + const testPath = getDataDir("scanDirectory") + let count = 0 + + const expectedPaths = ["1", "2", "3", "subdir", "subdir/4"].map((f) => join(testPath, f)) + + for await (const item of scanDirectory(testPath)) { + expect(expectedPaths).to.include(item.path) + count++ + } + + expect(count).to.eq(5) + }) + + it("should filter files based on filter function", async () => { + const testPath = getDataDir("scanDirectory") + const filterFunc = (item) => !item.includes("scanDirectory/subdir") + const expectedPaths = ["1", "2", "3"].map((f) => join(testPath, f)) + + let count = 0 + + for await (const item of scanDirectory(testPath, { filter: filterFunc })) { + expect(expectedPaths).to.include(item.path) + count++ + } + + expect(count).to.eq(3) + }) + }) + + describe("getChildDirNames", () => { + it("should return the names of all none hidden directories in the parent directory", async () => { + const testPath = getDataDir("get-child-dir-names") + expect(await getChildDirNames(testPath)).to.eql(["a", "b"]) + }) + }) + + describe("toCygwinPath", () => { + it("should convert a win32 path to a cygwin path", () => { + const path = "C:\\some\\path" + expect(toCygwinPath(path)).to.equal("/cygdrive/c/some/path") + }) + + it("should retain a trailing slash", () => { + const path = "C:\\some\\path\\" + expect(toCygwinPath(path)).to.equal("/cygdrive/c/some/path/") + }) + }) +}) diff --git a/garden-service/test/unit/src/util/util.ts b/garden-service/test/unit/src/util/util.ts index 264fc213c8..0712831a39 100644 --- a/garden-service/test/unit/src/util/util.ts +++ b/garden-service/test/unit/src/util/util.ts @@ -1,10 +1,5 @@ import { expect } from "chai" -import { join } from "path" -import { getDataDir } from "../../../helpers" import { - scanDirectory, - getChildDirNames, - toCygwinPath, pickKeys, getEnvVarName, deepOmitUndefined, @@ -13,56 +8,6 @@ import { import { expectError } from "../../../helpers" describe("util", () => { - describe("scanDirectory", () => { - it("should iterate through all files in a directory", async () => { - const testPath = getDataDir("scanDirectory") - let count = 0 - - const expectedPaths = ["1", "2", "3", "subdir", "subdir/4"].map((f) => join(testPath, f)) - - for await (const item of scanDirectory(testPath)) { - expect(expectedPaths).to.include(item.path) - count++ - } - - expect(count).to.eq(5) - }) - - it("should filter files based on filter function", async () => { - const testPath = getDataDir("scanDirectory") - const filterFunc = (item) => !item.includes("scanDirectory/subdir") - const expectedPaths = ["1", "2", "3"].map((f) => join(testPath, f)) - - let count = 0 - - for await (const item of scanDirectory(testPath, { filter: filterFunc })) { - expect(expectedPaths).to.include(item.path) - count++ - } - - expect(count).to.eq(3) - }) - }) - - describe("getChildDirNames", () => { - it("should return the names of all none hidden directories in the parent directory", async () => { - const testPath = getDataDir("get-child-dir-names") - expect(await getChildDirNames(testPath)).to.eql(["a", "b"]) - }) - }) - - describe("toCygwinPath", () => { - it("should convert a win32 path to a cygwin path", () => { - const path = "C:\\some\\path" - expect(toCygwinPath(path)).to.equal("/cygdrive/c/some/path") - }) - - it("should retain a trailing slash", () => { - const path = "C:\\some\\path\\" - expect(toCygwinPath(path)).to.equal("/cygdrive/c/some/path/") - }) - }) - describe("getEnvVarName", () => { it("should translate the service name to a name appropriate for env variables", async () => { expect(getEnvVarName("service-b")).to.equal("SERVICE_B") diff --git a/garden-service/test/unit/src/vcs/git.ts b/garden-service/test/unit/src/vcs/git.ts index 9a0092c0e3..5c7f3ff125 100644 --- a/garden-service/test/unit/src/vcs/git.ts +++ b/garden-service/test/unit/src/vcs/git.ts @@ -1,7 +1,6 @@ import { expect } from "chai" -import dedent = require("dedent") import * as tmp from "tmp-promise" -import { createFile, writeFile, realpath } from "fs-extra" +import { createFile, writeFile, realpath, mkdir } from "fs-extra" import { join, resolve } from "path" import { expectError } from "../../../helpers" @@ -25,30 +24,75 @@ describe("GitHandler", () => { await tmpDir.cleanup() }) - describe("getDirtyFiles", () => { + describe("getFiles", () => { it("should work with no commits in repo", async () => { - expect(await handler.getDirtyFiles(tmpPath)).to.eql([]) + expect(await handler.getFiles(tmpPath)).to.eql([]) }) - it("should return modified files as absolute paths", async () => { + it("should return tracked files as absolute paths with hash", async () => { const path = resolve(tmpPath, "foo.txt") await createFile(path) + await writeFile(path, "my change") await git("add", ".") await git("commit", "-m", "foo") - expect(await handler.getDirtyFiles(tmpPath)).to.eql([]) + const hash = "6e1ab2d7d26c1c66f27fea8c136e13c914e3f137" + + expect(await handler.getFiles(tmpPath)).to.eql([ + { path, hash }, + ]) + }) + + it("should return the correct hash on a modified file", async () => { + const path = resolve(tmpPath, "foo.txt") + + await createFile(path) + await git("add", ".") + await git("commit", "-m", "foo") await writeFile(path, "my change") + const hash = "6e1ab2d7d26c1c66f27fea8c136e13c914e3f137" - expect(await handler.getDirtyFiles(tmpPath)).to.eql([path]) + expect(await handler.getFiles(tmpPath)).to.eql([ + { path, hash }, + ]) }) - it("should return untracked files as absolute paths", async () => { + it("should return untracked files as absolute paths with hash", async () => { await createFile(join(tmpPath, "foo.txt")) + const hash = "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391" + + expect(await handler.getFiles(tmpPath)).to.eql([ + { path: resolve(tmpPath, "foo.txt"), hash }, + ]) + }) + + it("should return untracked files in untracked directory", async () => { + const dirPath = join(tmpPath, "dir") + await mkdir(dirPath) + await createFile(join(dirPath, "file.txt")) + const hash = "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391" + + expect(await handler.getFiles(dirPath)).to.eql([ + { path: resolve(dirPath, "file.txt"), hash }, + ]) + }) + + it("should filter out files that don't match the include filter, if specified", async () => { + const path = resolve(tmpPath, "foo.txt") + await createFile(path) + + expect(await handler.getFiles(tmpPath, [])).to.eql([]) + }) + + it("should include files that match the include filter, if specified", async () => { + const path = resolve(tmpPath, "foo.txt") + const hash = "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391" + await createFile(path) - expect(await handler.getDirtyFiles(tmpPath)).to.eql([ - resolve(tmpPath, "foo.txt"), + expect(await handler.getFiles(tmpPath, ["foo.*"])).to.eql([ + { path, hash }, ]) }) }) @@ -57,27 +101,27 @@ describe("GitHandler", () => { describe("git", () => { describe("getCommitIdFromRefList", () => { it("should get the commit id from a list of commit ids and refs", () => { - const refList = dedent` - abcde ref/heads/master - 1234 ref/heads/master - foobar ref/heads/master - ` + const refList = [ + "abcde ref/heads/master", + "1234 ref/heads/master", + "foobar ref/heads/master", + ] expect(getCommitIdFromRefList(refList)).to.equal("abcde") }) it("should get the commit id from a list of commit ids without refs", () => { - const refList = dedent` - abcde - 1234 ref/heads/master - foobar ref/heads/master - ` + const refList = [ + "abcde", + "1234 ref/heads/master", + "foobar ref/heads/master", + ] expect(getCommitIdFromRefList(refList)).to.equal("abcde") }) it("should get the commit id from a single commit id / ref pair", () => { - const refList = "abcde ref/heads/master" + const refList = ["abcde ref/heads/master"] expect(getCommitIdFromRefList(refList)).to.equal("abcde") }) it("should get the commit id from single commit id without a ref", () => { - const refList = "abcde" + const refList = ["abcde"] expect(getCommitIdFromRefList(refList)).to.equal("abcde") }) }) diff --git a/garden-service/test/unit/src/vcs/base.ts b/garden-service/test/unit/src/vcs/vcs.ts similarity index 52% rename from garden-service/test/unit/src/vcs/base.ts rename to garden-service/test/unit/src/vcs/vcs.ts index 17fcda6d5a..c242189903 100644 --- a/garden-service/test/unit/src/vcs/base.ts +++ b/garden-service/test/unit/src/vcs/vcs.ts @@ -4,31 +4,27 @@ import { TreeVersions, TreeVersion, getVersionString, - getLatestDirty, -} from "../../../../src/vcs/base" +} from "../../../../src/vcs/vcs" import { projectRootA, makeTestGardenA, makeTestGarden, getDataDir } from "../../../helpers" import { expect } from "chai" import { cloneDeep } from "lodash" import { Garden } from "../../../../src/garden" import { ModuleConfigContext } from "../../../../src/config/config-context" import { ModuleConfig } from "../../../../src/config/module" +import { GitHandler } from "../../../../src/vcs/git" +import { resolve } from "path" class TestVcsHandler extends VcsHandler { name = "test" private testVersions: TreeVersions = {} - async getLatestCommit() { - return NEW_MODULE_VERSION - } - - async getDirtyFiles() { + async getFiles() { return [] } async getTreeVersion(path: string) { return this.testVersions[path] || { - latestCommit: NEW_MODULE_VERSION, - dirtyTimestamp: null, + contentHash: NEW_MODULE_VERSION, } } @@ -51,8 +47,8 @@ describe("VcsHandler", () => { // note: module-a has a version file with this content const versionA = { - latestCommit: "1234567890", - dirtyTimestamp: null, + contentHash: "1234567890", + files: [], } beforeEach(async () => { @@ -60,14 +56,56 @@ describe("VcsHandler", () => { garden = await makeTestGardenA() }) + describe("getTreeVersion", () => { + const includeProjectRoot = getDataDir("test-projects", "include-field") + + it("should respect the include field, if specified", async () => { + const includeGarden = await makeTestGarden(includeProjectRoot) + const module = await includeGarden.resolveModuleConfig("module-a") + const includeHandler = new GitHandler(includeProjectRoot) + + const withInclude = await includeHandler.getTreeVersion(module.path, module.include!) + const withoutInclude = await includeHandler.getTreeVersion(module.path, null) + + expect(withInclude).to.eql({ + contentHash: "6413e73ab3", + files: [ + resolve(module.path, "yes.txt"), + ], + }) + + expect(withoutInclude).to.eql({ + contentHash: "80077a6c44", + files: [ + resolve(module.path, "garden.yml"), + resolve(module.path, "nope.txt"), + resolve(module.path, "yes.txt"), + ], + }) + }) + + it("should call getTreeVersion if there is no version file", async () => { + const module = await garden.resolveModuleConfig("module-b") + + const version = { + contentHash: "qwerty", + files: [], + } + handler.setTestVersion(module.path, version) + + const result = await handler.resolveTreeVersion(module.path, null) + expect(result).to.eql(version) + }) + }) + describe("resolveTreeVersion", () => { it("should return the version from a version file if it exists", async () => { const module = await garden.resolveModuleConfig("module-a") - const result = await handler.resolveTreeVersion(module.path) + const result = await handler.resolveTreeVersion(module.path, null) expect(result).to.eql({ - latestCommit: "1234567890", - dirtyTimestamp: null, + contentHash: "1234567890", + files: [], }) }) @@ -75,12 +113,12 @@ describe("VcsHandler", () => { const module = await garden.resolveModuleConfig("module-b") const version = { - latestCommit: "qwerty", - dirtyTimestamp: 456, + contentHash: "qwerty", + files: [], } handler.setTestVersion(module.path, version) - const result = await handler.resolveTreeVersion(module.path) + const result = await handler.resolveTreeVersion(module.path, null) expect(result).to.eql(version) }) }) @@ -107,76 +145,60 @@ describe("VcsHandler", () => { }) it("should return a different version for a module when a variable used by it changes", async () => { - const moduleABeforeVersion = getVersionString(moduleABefore, [], null) - const moduleAAfterVersion = getVersionString(moduleAAfter, [], null) + const moduleABeforeVersion = getVersionString(moduleABefore, []) + const moduleAAfterVersion = getVersionString(moduleAAfter, []) expect(moduleABeforeVersion).to.not.eql(moduleAAfterVersion) }) it("should return the same version for a module when a variable not used by it changes", async () => { - const moduleBBeforeVersion = getVersionString(moduleBBefore, [], null) - const moduleBAfterVersion = getVersionString(moduleBAfter, [], null) + const moduleBBeforeVersion = getVersionString(moduleBBefore, []) + const moduleBAfterVersion = getVersionString(moduleBAfter, []) expect(moduleBBeforeVersion).to.eql(moduleBAfterVersion) }) - }) context("internal helpers", () => { const namedVersionA = { name: "module-a", - latestCommit: "qwerty", - dirtyTimestamp: null, + contentHash: "qwerty", + files: [], } const namedVersionB = { name: "module-b", - latestCommit: "qwerty", - dirtyTimestamp: 123, + contentHash: "qwerty", + files: [], } const namedVersionC = { name: "module-c", - latestCommit: "qwerty", - dirtyTimestamp: 456, + contentHash: "qwerty", + files: [], } const namedVersions = [namedVersionA, namedVersionB, namedVersionC] describe("hashVersions", () => { - it("is stable with respect to key order in moduleConfig", async () => { const originalConfig = await garden.resolveModuleConfig("module-a") const stirredConfig = cloneDeep(originalConfig) delete stirredConfig.name stirredConfig.name = originalConfig.name - expect(getVersionString(originalConfig, namedVersions, null)) - .to.eql(getVersionString(stirredConfig, namedVersions, null)) + expect(getVersionString(originalConfig, namedVersions)) + .to.eql(getVersionString(stirredConfig, namedVersions)) }) it("is stable with respect to named version order", async () => { const config = await garden.resolveModuleConfig("module-a") - expect(getVersionString(config, [namedVersionA, namedVersionB, namedVersionC], null)) - .to.eql(getVersionString(config, [namedVersionB, namedVersionA, namedVersionC], null)) - }) - - }) - - describe("getLatestDirty", () => { - - it("returns the latest dirty timestamp if one or more versions provided have one", () => { - expect(getLatestDirty(namedVersions)).to.eql(456) - }) - - it("returns null if none of the versions provided has a dirty has one", () => { - expect(getLatestDirty([namedVersionA])).to.eql(null) + expect(getVersionString(config, [namedVersionA, namedVersionB, namedVersionC])) + .to.eql(getVersionString(config, [namedVersionB, namedVersionA, namedVersionC])) }) - }) - }) describe("resolveVersion", () => { @@ -186,110 +208,26 @@ describe("VcsHandler", () => { const result = await handler.resolveVersion(module, []) expect(result).to.eql({ - versionString: getVersionString(module, [{ ...versionA, name: "module-a" }], null), - dirtyTimestamp: null, - dependencyVersions: {}, - }) - }) - - it("should return module version if there are no dependencies and properly handle a dirty timestamp", async () => { - const module = await garden.resolveModuleConfig("module-b") - const latestCommit = "abcdef" - const dirtyTimestamp = 1234 - const version = { - latestCommit, - dirtyTimestamp, - } - - handler.setTestVersion(module.path, version) - - const result = await handler.resolveVersion(module, []) - - expect(result).to.eql({ - dirtyTimestamp, - versionString: getVersionString(module, [{ ...version, name: "module-b" }], dirtyTimestamp), + versionString: getVersionString(module, [{ ...versionA, name: "module-a" }]), dependencyVersions: {}, + files: [], }) }) - it("should return the dirty version if there is a single one", async () => { - const [moduleA, moduleB, moduleC] = await garden.resolveModuleConfigs(["module-a", "module-b", "module-c"]) - - const dirtyTimestamp = 123 - - const versionB = { - latestCommit: "qwerty", - dirtyTimestamp: null, - } - handler.setTestVersion(moduleB.path, versionB) - - const versionStringC = "asdfgh" - const versionC = { - dirtyTimestamp, - latestCommit: versionStringC, - } - handler.setTestVersion(moduleC.path, versionC) - - expect(await handler.resolveVersion(moduleC, [moduleA, moduleB])).to.eql({ - dirtyTimestamp, - versionString: getVersionString(moduleC, [ - { ...versionA, name: "module-a" }, - { ...versionB, name: "module-b" }, - { ...versionC, name: "module-c" }, - ], 123), - dependencyVersions: { - "module-a": versionA, - "module-b": versionB, - }, - }) - }) - - it("should return the latest dirty version if there are multiple", async () => { - const [moduleA, moduleB, moduleC] = await garden.resolveModuleConfigs(["module-a", "module-b", "module-c"]) - - const latestDirty = 456 - - const versionB = { - latestCommit: "qwerty", - dirtyTimestamp: latestDirty, - } - handler.setTestVersion(moduleB.path, versionB) - - const versionStringC = "asdfgh" - const versionC = { - latestCommit: versionStringC, - dirtyTimestamp: 123, - } - handler.setTestVersion(moduleC.path, versionC) - - expect(await handler.resolveVersion(moduleC, [moduleA, moduleB])).to.eql({ - versionString: getVersionString(moduleC, [ - { ...versionA, name: "module-a" }, - { ...versionB, name: "module-b" }, - { ...versionC, name: "module-c" }, - ], latestDirty), - dirtyTimestamp: latestDirty, - dependencyVersions: { - "module-a": versionA, - "module-b": versionB, - }, - }) - }) - - it("should hash together the version of the module and all dependencies if none are dirty", async () => { + it("should hash together the version of the module and all dependencies", async () => { const [moduleA, moduleB, moduleC] = await garden.resolveModuleConfigs(["module-a", "module-b", "module-c"]) const versionStringB = "qwerty" const versionB = { - latestCommit: versionStringB, - dirtyTimestamp: null, + contentHash: versionStringB, + files: [], } handler.setTestVersion(moduleB.path, versionB) const versionStringC = "asdfgh" const versionC = { - latestCommit: versionStringC, - dirtyTimestamp: null, + contentHash: versionStringC, + files: [], } handler.setTestVersion(moduleC.path, versionC) @@ -298,49 +236,13 @@ describe("VcsHandler", () => { { ...versionA, name: "module-a" }, { ...versionB, name: "module-b" }, { ...versionC, name: "module-c" }, - ], null), - dirtyTimestamp: null, + ]), dependencyVersions: { "module-a": versionA, "module-b": versionB, }, + files: [], }) }) - - it( - "should hash together the dirty versions and add the timestamp if there are multiple with same timestamp", - async () => { - - const [moduleA, moduleB, moduleC] = await garden.resolveModuleConfigs(["module-a", "module-b", "module-c"]) - - const dirtyTimestamp = 1234 - - const versionStringB = "qwerty" - const versionB = { - dirtyTimestamp, - latestCommit: versionStringB, - } - handler.setTestVersion(moduleB.path, versionB) - - const versionStringC = "asdfgh" - const versionC = { - dirtyTimestamp, - latestCommit: versionStringC, - } - handler.setTestVersion(moduleC.path, versionC) - - expect(await handler.resolveVersion(moduleC, [moduleA, moduleB])).to.eql({ - versionString: getVersionString(moduleC, [ - { ...versionA, name: "module-a" }, - { ...versionB, name: "module-b" }, - { ...versionC, name: "module-c" }, - ], dirtyTimestamp), - dirtyTimestamp, - dependencyVersions: { - "module-a": versionA, - "module-b": versionB, - }, - }) - }) }) }) diff --git a/garden-service/test/unit/src/watch.ts b/garden-service/test/unit/src/watch.ts index dcc0c1ab2a..90f988c550 100644 --- a/garden-service/test/unit/src/watch.ts +++ b/garden-service/test/unit/src/watch.ts @@ -2,18 +2,22 @@ import { resolve } from "path" import { TestGarden, dataDir, makeTestGarden } from "../../helpers" import { expect } from "chai" import { CacheContext, pathToCacheContext } from "../../../src/cache" +import { CONFIG_FILENAME } from "../../../src/constants" import pEvent = require("p-event") +import { createFile, remove, pathExists } from "fs-extra" describe("Watcher", () => { let garden: TestGarden let modulePath: string let doubleModulePath: string + let includeModulePath: string let moduleContext: CacheContext before(async () => { garden = await makeTestGarden(resolve(dataDir, "test-project-watch")) modulePath = resolve(garden.projectRoot, "module-a") doubleModulePath = resolve(garden.projectRoot, "double-module") + includeModulePath = resolve(garden.projectRoot, "with-include") moduleContext = pathToCacheContext(modulePath) await garden.startWatcher(await garden.getConfigGraph()) }) @@ -35,47 +39,53 @@ describe("Watcher", () => { } it("should emit a moduleConfigChanged changed event when module config is changed", async () => { - const path = resolve(modulePath, "garden.yml") + const path = resolve(modulePath, CONFIG_FILENAME) emitEvent("change", path) expect(garden.events.log).to.eql([ { name: "moduleConfigChanged", payload: { names: ["module-a"], path } }, ]) }) + it("should emit a moduleConfigChanged event when module config is changed and include field is set", async () => { + const path = resolve(includeModulePath, CONFIG_FILENAME) + emitEvent("change", path) + expect(garden.events.log).to.eql([ + { name: "moduleConfigChanged", payload: { names: ["with-include"], path } }, + ]) + }) + it("should clear all module caches when a module config is changed", async () => { - emitEvent("change", resolve(modulePath, "garden.yml")) + emitEvent("change", resolve(modulePath, CONFIG_FILENAME)) expect(garden.cache.getByContext(moduleContext)).to.eql(new Map()) }) it("should emit a projectConfigChanged changed event when project config is changed", async () => { - emitEvent("change", resolve(garden.projectRoot, "garden.yml")) + emitEvent("change", resolve(garden.projectRoot, CONFIG_FILENAME)) expect(garden.events.log).to.eql([ { name: "projectConfigChanged", payload: {} }, ]) }) it("should emit a projectConfigChanged changed event when project config is removed", async () => { - emitEvent("unlink", resolve(garden.projectRoot, "garden.yml")) + emitEvent("unlink", resolve(garden.projectRoot, CONFIG_FILENAME)) expect(garden.events.log).to.eql([ { name: "projectConfigChanged", payload: {} }, ]) }) it("should clear all module caches when project config is changed", async () => { - emitEvent("change", resolve(garden.projectRoot, "garden.yml")) + emitEvent("change", resolve(garden.projectRoot, CONFIG_FILENAME)) expect(garden.cache.getByContext(moduleContext)).to.eql(new Map()) }) it("should emit a configAdded event when adding a garden.yml file", async () => { - const path = resolve(garden.projectRoot, "module-b", "garden.yml") + const path = resolve(garden.projectRoot, "module-b", CONFIG_FILENAME) emitEvent("add", path) - expect(garden.events.log).to.eql([ - { name: "configAdded", payload: { path } }, - ]) + expect(await waitForEvent("configAdded")).to.eql({ path }) }) it("should emit a configRemoved event when removing a garden.yml file", async () => { - const path = resolve(garden.projectRoot, "module-b", "garden.yml") + const path = resolve(garden.projectRoot, "module-b", CONFIG_FILENAME) emitEvent("unlink", path) expect(garden.events.log).to.eql([ { name: "configRemoved", payload: { path } }, @@ -83,7 +93,6 @@ describe("Watcher", () => { }) context("should emit a moduleSourcesChanged event", () => { - it("containing the module's name when one of its files is changed", async () => { const pathChanged = resolve(modulePath, "foo.txt") emitEvent("change", pathChanged) @@ -92,6 +101,24 @@ describe("Watcher", () => { ]) }) + it("if a file is changed and it matches a module's include list", async () => { + const pathChanged = resolve(includeModulePath, "subdir", "foo2.txt") + emitEvent("change", pathChanged) + expect(garden.events.log).to.eql([ + { name: "moduleSourcesChanged", payload: { names: ["with-include"], pathChanged } }, + ]) + }) + + it("if a file is added to a module", async () => { + const pathChanged = resolve(modulePath, "new.txt") + try { + await createFile(pathChanged) + expect(await waitForEvent("moduleSourcesChanged")).to.eql({ names: ["module-a"], pathChanged }) + } finally { + await pathExists(pathChanged) && await remove(pathChanged) + } + }) + it("containing both modules' names when a source file is changed for two co-located modules", async () => { const pathChanged = resolve(doubleModulePath, "foo.txt") emitEvent("change", pathChanged) @@ -99,7 +126,24 @@ describe("Watcher", () => { { name: "moduleSourcesChanged", payload: { names: ["module-b", "module-c"], pathChanged } }, ]) }) + }) + it("should not emit moduleSourcesChanged if file is changed and doesn't match module's include list", async () => { + const pathChanged = resolve(includeModulePath, "foo.txt") + emitEvent("change", pathChanged) + expect(garden.events.log).to.eql([]) + }) + + it("should not emit moduleSourcesChanged if file is changed and it's in a gardenignore in the module", async () => { + const pathChanged = resolve(modulePath, "module-excluded.txt") + emitEvent("change", pathChanged) + expect(garden.events.log).to.eql([]) + }) + + it("should not emit moduleSourcesChanged if file is changed and it's in a gardenignore in the project", async () => { + const pathChanged = resolve(modulePath, "project-excluded.txt") + emitEvent("change", pathChanged) + expect(garden.events.log).to.eql([]) }) it("should clear a module's cache when a module file is changed", async () => { @@ -111,7 +155,7 @@ describe("Watcher", () => { it("should emit a configAdded event when a directory is added that contains a garden.yml file", async () => { emitEvent("addDir", modulePath) expect(await waitForEvent("configAdded")).to.eql({ - path: resolve(modulePath, "garden.yml"), + path: resolve(modulePath, CONFIG_FILENAME), }) })