diff --git a/garden-service/src/garden.ts b/garden-service/src/garden.ts index 51d5f3d77b..65c5505778 100644 --- a/garden-service/src/garden.ts +++ b/garden-service/src/garden.ts @@ -85,20 +85,22 @@ interface ModuleConfigResolveOpts extends ContextResolveOpts { } export interface GardenParams { - buildDir: BuildDir, - environmentName: string, - gardenDirPath: string, - opts: GardenOpts, - plugins: Plugins, - projectName: string, - projectRoot: string, - projectSources?: SourceConfig[], - providerConfigs: ProviderConfig[], - variables: PrimitiveMap, - workingCopyId: string, + buildDir: BuildDir + environmentName: string dotIgnoreFiles: string[] + gardenDirPath: string + log: LogEntry moduleIncludePatterns?: string[] moduleExcludePatterns?: string[] + opts: GardenOpts + plugins: Plugins + projectName: string + projectRoot: string + projectSources?: SourceConfig[] + providerConfigs: ProviderConfig[] + variables: PrimitiveMap + vcs: VcsHandler + workingCopyId: string } export class Garden { @@ -139,6 +141,7 @@ export class Garden { this.buildDir = params.buildDir this.environmentName = params.environmentName this.gardenDirPath = params.gardenDirPath + this.log = params.log this.opts = params.opts this.projectName = params.projectName this.projectRoot = params.projectRoot @@ -151,6 +154,7 @@ export class Garden { this.moduleExcludePatterns = params.moduleExcludePatterns || [] this.asyncLock = new AsyncLock() this.persistent = !!params.opts.persistent + this.vcs = params.vcs // make sure we're on a supported platform const currentPlatform = platform() @@ -165,9 +169,7 @@ export class Garden { } this.modulesScanned = false - this.log = this.opts.log || getLogger().placeholder() // TODO: Support other VCS options. - this.vcs = new GitHandler(this.gardenDirPath, this.dotIgnoreFiles) this.configStore = new LocalConfigStore(this.gardenDirPath) this.globalConfigStore = new GlobalConfigStore() this.cache = new TreeCache() @@ -220,11 +222,16 @@ export class Garden { gardenDirPath = resolve(projectRoot, gardenDirPath || DEFAULT_GARDEN_DIR_NAME) const buildDir = await BuildDir.factory(projectRoot, gardenDirPath) const workingCopyId = await getWorkingCopyId(gardenDirPath) + const log = opts.log || getLogger().placeholder() // We always exclude the garden dir const gardenDirExcludePattern = `${relative(projectRoot, gardenDirPath)}/**/*` const moduleExcludePatterns = [...((config.modules || {}).exclude || []), gardenDirExcludePattern] + // Ensure the project root is in a git repo + const vcs = new GitHandler(gardenDirPath, config.dotIgnoreFiles) + await vcs.getRepoRoot(log, projectRoot) + const garden = new this({ projectRoot, projectName, @@ -240,6 +247,8 @@ export class Garden { workingCopyId, dotIgnoreFiles: config.dotIgnoreFiles, moduleIncludePatterns: (config.modules || {}).include, + log, + vcs, }) as InstanceType return garden diff --git a/garden-service/src/vcs/git.ts b/garden-service/src/vcs/git.ts index 30cb4b1948..677f87adc4 100644 --- a/garden-service/src/vcs/git.ts +++ b/garden-service/src/vcs/git.ts @@ -73,13 +73,32 @@ export class GitHandler extends VcsHandler { } } + async getRepoRoot(log: LogEntry, path: string) { + const git = this.gitCli(log, path) + + try { + return (await git("rev-parse", "--show-toplevel"))[0] + } catch (err) { + if (err.exitCode === 128) { + // Throw nice error when we detect that we're not in a repo root + throw new RuntimeError(deline` + Path ${path} is not in a git repository root. Garden must be run from within a git repo. + Please run \`git init\` if you're starting a new project and repository, or move the project to an + existing repository, and try again. + `, { path }) + } else { + throw err + } + } + } + /** * Returns a list of files, along with file hashes, under the given path, taking into account the configured * .ignore files, and the specified include/exclude filters. */ async getFiles({ log, path, include, exclude }: GetFilesParams): Promise { const git = this.gitCli(log, path) - const gitRoot = (await git("rev-parse", "--show-toplevel"))[0] + const gitRoot = await this.getRepoRoot(log, path) // List modified files, so that we can ensure we have the right hash for them later const modified = new Set((await this.getModifiedFiles(git, path)) diff --git a/garden-service/src/vcs/vcs.ts b/garden-service/src/vcs/vcs.ts index 9ca2ab974f..4100bfc114 100644 --- a/garden-service/src/vcs/vcs.ts +++ b/garden-service/src/vcs/vcs.ts @@ -88,6 +88,7 @@ export abstract class VcsHandler { constructor(protected gardenDirPath: string, protected ignoreFiles: string[]) { } abstract name: string + abstract async getRepoRoot(log: LogEntry, path: string): Promise abstract async getFiles(params: GetFilesParams): Promise abstract async ensureRemoteSource(params: RemoteSourceParams): Promise abstract async updateRemoteSource(params: RemoteSourceParams): Promise diff --git a/garden-service/test/unit/src/garden.ts b/garden-service/test/unit/src/garden.ts index 56bb6da049..37e9a6b74b 100644 --- a/garden-service/test/unit/src/garden.ts +++ b/garden-service/test/unit/src/garden.ts @@ -1,5 +1,6 @@ import { expect } from "chai" import td from "testdouble" +import tmp from "tmp-promise" import { join, resolve } from "path" import { Garden } from "../../../src/garden" import { @@ -33,6 +34,8 @@ import { keyBy, set } from "lodash" import stripAnsi from "strip-ansi" import { joi } from "../../../src/config/common" import { defaultDotIgnoreFiles } from "../../../src/util/fs" +import { realpath, writeFile } from "fs-extra" +import { dedent } from "../../../src/util/string" describe("Garden", () => { beforeEach(async () => { @@ -273,6 +276,21 @@ describe("Garden", () => { c: "c", }) }) + + it("should throw if project root is not in a git repo root", async () => { + const tmpDir = await tmp.dir({ unsafeCleanup: true }) + + try { + const tmpPath = await realpath(tmpDir.path) + await writeFile(join(tmpPath, "garden.yml"), dedent` + kind: Project + name: foo + `) + await expectError(async () => Garden.factory(tmpPath, {}), "runtime") + } finally { + await tmpDir.cleanup() + } + }) }) describe("resolveProviders", () => { diff --git a/garden-service/test/unit/src/vcs/git.ts b/garden-service/test/unit/src/vcs/git.ts index b94d0db66a..1f724ab783 100644 --- a/garden-service/test/unit/src/vcs/git.ts +++ b/garden-service/test/unit/src/vcs/git.ts @@ -18,6 +18,7 @@ import { getCommitIdFromRefList, parseGitUrl, GitHandler } from "../../../../src import { fixedExcludes } from "../../../../src/util/fs" import { LogEntry } from "../../../../src/logger/log-entry" import { hashRepoUrl } from "../../../../src/util/ext-source-util" +import { deline } from "../../../../src/util/string" // Overriding this to make sure any ignorefile name is respected const defaultIgnoreFilename = ".testignore" @@ -56,7 +57,7 @@ async function addToIgnore(tmpPath: string, pathToExclude: string, ignoreFilenam describe("GitHandler", () => { let tmpDir: tmp.DirectoryResult let tmpPath: string - let git + let git: any let handler: GitHandler let log: LogEntry @@ -74,6 +75,30 @@ describe("GitHandler", () => { await tmpDir.cleanup() }) + describe("getRepoRoot", () => { + it("should return the repo root if it is the same as the given path", async () => { + const path = tmpPath + expect(await handler.getRepoRoot(log, path)).to.equal(tmpPath) + }) + + it("should return the nearest repo root, given a subpath of that repo", async () => { + const dirPath = join(tmpPath, "dir") + await mkdir(dirPath) + expect(await handler.getRepoRoot(log, dirPath)).to.equal(tmpPath) + }) + + it("should throw a nice error when given a path outside of a repo", async () => { + await expectError( + () => handler.getRepoRoot(log, "/tmp"), + (err) => expect(err.message).to.equal(deline` + Path /tmp is not in a git repository root. Garden must be run from within a git repo. + Please run \`git init\` if you're starting a new project and repository, or move the project to + an existing repository, and try again. + `), + ) + }) + }) + describe("getFiles", () => { it("should work with no commits in repo", async () => { expect(await handler.getFiles({ path: tmpPath, log })).to.eql([]) diff --git a/garden-service/test/unit/src/vcs/vcs.ts b/garden-service/test/unit/src/vcs/vcs.ts index 6c81d6442e..9b194ad45e 100644 --- a/garden-service/test/unit/src/vcs/vcs.ts +++ b/garden-service/test/unit/src/vcs/vcs.ts @@ -25,6 +25,10 @@ class TestVcsHandler extends VcsHandler { name = "test" private testVersions: TreeVersions = {} + async getRepoRoot() { + return "/foo" + } + async getFiles() { return [] }