Skip to content

Commit

Permalink
improvement: better error when attempting to run outside of git repo
Browse files Browse the repository at this point in the history
Closes #1194
  • Loading branch information
edvald committed Sep 17, 2019
1 parent ce9a2fb commit 11887d7
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 15 deletions.
35 changes: 22 additions & 13 deletions garden-service/src/garden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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,
Expand All @@ -240,6 +247,8 @@ export class Garden {
workingCopyId,
dotIgnoreFiles: config.dotIgnoreFiles,
moduleIncludePatterns: (config.modules || {}).include,
log,
vcs,
}) as InstanceType<T>

return garden
Expand Down
21 changes: 20 additions & 1 deletion garden-service/src/vcs/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<VcsFile[]> {
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))
Expand Down
1 change: 1 addition & 0 deletions garden-service/src/vcs/vcs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>
abstract async getFiles(params: GetFilesParams): Promise<VcsFile[]>
abstract async ensureRemoteSource(params: RemoteSourceParams): Promise<string>
abstract async updateRemoteSource(params: RemoteSourceParams): Promise<void>
Expand Down
18 changes: 18 additions & 0 deletions garden-service/test/unit/src/garden.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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", () => {
Expand Down
27 changes: 26 additions & 1 deletion garden-service/test/unit/src/vcs/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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

Expand All @@ -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([])
Expand Down
4 changes: 4 additions & 0 deletions garden-service/test/unit/src/vcs/vcs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ class TestVcsHandler extends VcsHandler {
name = "test"
private testVersions: TreeVersions = {}

async getRepoRoot() {
return "/foo"
}

async getFiles() {
return []
}
Expand Down

0 comments on commit 11887d7

Please sign in to comment.