diff --git a/core/src/config/config-context.ts b/core/src/config/config-context.ts index 604358d470..41f82ce313 100644 --- a/core/src/config/config-context.ts +++ b/core/src/config/config-context.ts @@ -296,6 +296,33 @@ class ProjectContext extends ConfigContext { } } +class GitContext extends ConfigContext { + @schema( + joi + .string() + .description( + dedent` + The current Git branch, if available. Resolves to an empty string if HEAD is in a detached state + (e.g. when rebasing), or if the repository has no commits. + + When using remote sources, the branch used is that of the project/top-level repository (the one that contains + the project configuration). + + The branch is computed at the start of the Garden command's execution, and is not updated if the current + branch changes during the command's execution (which could happen, for example, when using watch-mode + commands). + ` + ) + .example("my-feature-branch") + ) + public branch: string + + constructor(root: ConfigContext, branch: string) { + super(root) + this.branch = branch + } +} + /** * This context is available for template strings in the `defaultEnvironment` field in project configs. */ @@ -310,17 +337,25 @@ export class DefaultEnvironmentContext extends ConfigContext { @schema(ProjectContext.getSchema().description("Information about the Garden project.")) public project: ProjectContext + @schema( + GitContext.getSchema().description("Information about the current state of the project's local git repository.") + ) + public git: GitContext + constructor({ projectName, artifactsPath, + branch, username, }: { projectName: string artifactsPath: string + branch: string username?: string }) { super() this.local = new LocalContext(this, artifactsPath, username) + this.git = new GitContext(this, branch) this.project = new ProjectContext(this, projectName) } } @@ -328,6 +363,7 @@ export class DefaultEnvironmentContext extends ConfigContext { export interface ProjectConfigContextParams { projectName: string artifactsPath: string + branch: string username?: string secrets: PrimitiveMap } @@ -340,16 +376,6 @@ export interface ProjectConfigContextParams { * `secrets`. */ export class ProjectConfigContext extends DefaultEnvironmentContext { - @schema( - LocalContext.getSchema().description( - "Context variables that are specific to the currently running environment/machine." - ) - ) - public local: LocalContext - - @schema(ProjectContext.getSchema().description("Information about the Garden project.")) - public project: ProjectContext - @schema( joiStringMap(joi.string().description("The secret's value.")) .description("A map of all secrets for this project in the current environment.") @@ -360,8 +386,8 @@ export class ProjectConfigContext extends DefaultEnvironmentContext { ) public secrets: PrimitiveMap - constructor({ projectName, artifactsPath, username, secrets }: ProjectConfigContextParams) { - super({ projectName, artifactsPath, username }) + constructor({ projectName, artifactsPath, branch, username, secrets }: ProjectConfigContextParams) { + super({ projectName, artifactsPath, branch, username }) this.secrets = secrets } } @@ -370,16 +396,6 @@ export class ProjectConfigContext extends DefaultEnvironmentContext { * This context is available for template strings for all `environments[]` fields (except name) */ export class EnvironmentConfigContext extends ProjectConfigContext { - @schema( - LocalContext.getSchema().description( - "Context variables that are specific to the currently running environment/machine." - ) - ) - public local: LocalContext - - @schema(ProjectContext.getSchema().description("Information about the Garden project.")) - public project: ProjectContext - @schema( joiVariables() .description("A map of all variables defined in the project configuration.") @@ -403,17 +419,19 @@ export class EnvironmentConfigContext extends ProjectConfigContext { constructor({ projectName, artifactsPath, + branch, username, variables, secrets, }: { projectName: string artifactsPath: string + branch: string username?: string variables: DeepPrimitiveMap secrets: PrimitiveMap }) { - super({ projectName, artifactsPath, username, secrets }) + super({ projectName, artifactsPath, branch, username, secrets }) this.variables = this.var = variables } } @@ -478,6 +496,7 @@ export class WorkflowConfigContext extends EnvironmentConfigContext { super({ projectName: garden.projectName, artifactsPath: garden.artifactsPath, + branch: garden.vcsBranch, username: garden.username, variables: garden.variables, secrets: garden.secrets, diff --git a/core/src/config/module-template.ts b/core/src/config/module-template.ts index 0226075249..50757b0c02 100644 --- a/core/src/config/module-template.ts +++ b/core/src/config/module-template.ts @@ -53,7 +53,7 @@ export async function resolveModuleTemplate( ...resource, modules: [], } - const context = new ProjectConfigContext(garden) + const context = new ProjectConfigContext({ ...garden, branch: garden.vcsBranch }) const resolved = resolveTemplateStrings(partial, context) // Validate the partial config @@ -109,7 +109,7 @@ export async function resolveTemplatedModule( templates: { [name: string]: ModuleTemplateConfig } ) { // Resolve template strings for fields - const resolved = resolveTemplateStrings(config, new ProjectConfigContext(garden)) + const resolved = resolveTemplateStrings(config, new ProjectConfigContext({ ...garden, branch: garden.vcsBranch })) const configType = "templated module " + resolved.name let resolvedSpec = omit(resolved.spec, "build") @@ -155,6 +155,7 @@ export async function resolveTemplatedModule( // Prepare modules and resolve templated names const context = new ModuleTemplateConfigContext({ ...garden, + branch: garden.vcsBranch, parentName: resolved.name, templateName: template.name, inputs, diff --git a/core/src/config/project.ts b/core/src/config/project.ts index 67d60a61f5..be6ba4cdf6 100644 --- a/core/src/config/project.ts +++ b/core/src/config/project.ts @@ -399,12 +399,14 @@ export function resolveProjectConfig({ defaultEnvironment, config, artifactsPath, + branch, username, secrets, }: { defaultEnvironment: string config: ProjectConfig artifactsPath: string + branch: string username: string secrets: PrimitiveMap }): ProjectConfig { @@ -419,7 +421,7 @@ export function resolveProjectConfig({ variables: config.variables, environments: [], }, - new ProjectConfigContext({ projectName: name, artifactsPath, username, secrets }) + new ProjectConfigContext({ projectName: name, artifactsPath, branch, username, secrets }) ) // Validate after resolving global fields @@ -495,12 +497,14 @@ export async function pickEnvironment({ projectConfig, envString, artifactsPath, + branch, username, secrets, }: { projectConfig: ProjectConfig envString: string artifactsPath: string + branch: string username: string secrets: PrimitiveMap }) { @@ -527,7 +531,7 @@ export async function pickEnvironment({ // Resolve template strings in the environment config, except providers environmentConfig = resolveTemplateStrings( { ...environmentConfig, providers: [] }, - new EnvironmentConfigContext({ projectName, artifactsPath, username, variables: projectVariables, secrets }) + new EnvironmentConfigContext({ projectName, artifactsPath, branch, username, variables: projectVariables, secrets }) ) environmentConfig = validateWithPath({ diff --git a/core/src/garden.ts b/core/src/garden.ts index a80f9d5735..0021a6cf06 100644 --- a/core/src/garden.ts +++ b/core/src/garden.ts @@ -127,6 +127,7 @@ export interface GardenOpts { export interface GardenParams { artifactsPath: string + vcsBranch: string buildDir: BuildDir clientAuthToken: string | null enterpriseDomain: string | null @@ -193,6 +194,7 @@ export class Garden { public readonly buildDir: BuildDir public readonly gardenDirPath: string public readonly artifactsPath: string + public readonly vcsBranch: string public readonly opts: GardenOpts private readonly providerConfigs: GenericProviderConfig[] public readonly workingCopyId: string @@ -218,6 +220,7 @@ export class Garden { this.gardenDirPath = params.gardenDirPath this.log = params.log this.artifactsPath = params.artifactsPath + this.vcsBranch = params.vcsBranch this.opts = params.opts this.rawOutputs = params.outputs this.production = params.production @@ -308,9 +311,13 @@ export class Garden { // Connect to the state storage await ensureConnected() + const { sources: projectSources, path: projectRoot } = config + + const vcsBranch = (await new GitHandler(projectRoot, gardenDirPath, []).getBranchName(log, projectRoot)) || "" + const defaultEnvironmentName = resolveTemplateString( config.defaultEnvironment, - new DefaultEnvironmentContext({ projectName, artifactsPath, username: _username }) + new DefaultEnvironmentContext({ projectName, artifactsPath, branch: vcsBranch, username: _username }) ) as string const defaultEnvironment = getDefaultEnvironmentName(defaultEnvironmentName, config) @@ -336,16 +343,18 @@ export class Garden { defaultEnvironment: defaultEnvironmentName, config, artifactsPath, + branch: vcsBranch, username: _username, secrets, }) - const { sources: projectSources, path: projectRoot } = config + const vcs = new GitHandler(projectRoot, gardenDirPath, config.dotIgnoreFiles) let { namespace, providers, variables, production } = await pickEnvironment({ projectConfig: config, envString: environmentStr, artifactsPath, + branch: vcsBranch, username: _username, secrets, }) @@ -364,12 +373,9 @@ export class Garden { ...fixedProjectExcludes, ] - // Ensure the project root is in a git repo - const vcs = new GitHandler(projectRoot, gardenDirPath, config.dotIgnoreFiles) - await vcs.getRepoRoot(log, projectRoot) - const garden = new this({ artifactsPath, + vcsBranch, sessionId, clientAuthToken, enterpriseDomain, diff --git a/core/src/vcs/git.ts b/core/src/vcs/git.ts index eaed1f11bb..61e7b34312 100644 --- a/core/src/vcs/git.ts +++ b/core/src/vcs/git.ts @@ -85,7 +85,6 @@ export class GitHandler extends VcsHandler { if (this.repoRoots.has(path)) { return this.repoRoots.get(path) } - const git = this.gitCli(log, path) try { @@ -95,14 +94,7 @@ export class GitHandler extends VcsHandler { } 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 } - ) + throw new RuntimeError(notInRepoRootErrorMessage(path), { path }) } else { throw err } @@ -417,4 +409,30 @@ export class GitHandler extends VcsHandler { } return undefined } + + async getBranchName(log: LogEntry, path: string): Promise { + const git = this.gitCli(log, path) + try { + return (await git("rev-parse", "--abbrev-ref", "HEAD"))[0] + } catch (err) { + if (err.exitCode === 128) { + try { + // If this doesn't throw, then we're in a repo with no commits, or with a detached HEAD. + await git("rev-parse", "--show-toplevel") + return undefined + } catch (notInRepoError) { + // Throw nice error when we detect that we're not in a repo root + throw new RuntimeError(notInRepoRootErrorMessage(path), { path }) + } + } else { + throw err + } + } + } } + +const notInRepoRootErrorMessage = (path: string) => 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. + ` diff --git a/core/src/vcs/vcs.ts b/core/src/vcs/vcs.ts index 08fcf35aac..f0eefbe375 100644 --- a/core/src/vcs/vcs.ts +++ b/core/src/vcs/vcs.ts @@ -75,6 +75,7 @@ export abstract class VcsHandler { abstract async ensureRemoteSource(params: RemoteSourceParams): Promise abstract async updateRemoteSource(params: RemoteSourceParams): Promise abstract async getOriginName(log: LogEntry): Promise + abstract async getBranchName(log: LogEntry, path: string): Promise async getTreeVersion(log: LogEntry, projectName: string, moduleConfig: ModuleConfig): Promise { const configPath = moduleConfig.configPath diff --git a/core/test/unit/src/config/config-context.ts b/core/test/unit/src/config/config-context.ts index b456cdc9ab..1c6b51b373 100644 --- a/core/test/unit/src/config/config-context.ts +++ b/core/test/unit/src/config/config-context.ts @@ -7,6 +7,9 @@ */ import { expect } from "chai" +import { join } from "path" +import stripAnsi = require("strip-ansi") +import { keyBy } from "lodash" import { ConfigContext, ContextKey, @@ -20,13 +23,11 @@ import { ScanContext, } from "../../../../src/config/config-context" import { expectError, makeTestGardenA, TestGarden, projectRootA, makeTestGarden } from "../../../helpers" -import { join } from "path" import { joi } from "../../../../src/config/common" import { prepareRuntimeContext } from "../../../../src/runtime-context" import { Service } from "../../../../src/types/service" -import stripAnsi = require("strip-ansi") import { resolveTemplateString, resolveTemplateStrings } from "../../../../src/template-string" -import { keyBy } from "lodash" +import { exec } from "../../../../src/util/util" type TestValue = string | ConfigContext | TestValues | TestValueFunction type TestValueFunction = () => TestValue | Promise @@ -34,7 +35,13 @@ interface TestValues { [key: string]: TestValue } +let currentBranch + describe("ConfigContext", () => { + before(async () => { + currentBranch = (await exec("git", ["rev-parse", "--abbrev-ref", "HEAD"])).stdout + }) + class TestContext extends ConfigContext { constructor(obj: TestValues, root?: ConfigContext) { super(root) @@ -323,6 +330,7 @@ describe("ProjectConfigContext", () => { const c = new ProjectConfigContext({ projectName: "some-project", artifactsPath: "/tmp", + branch: "main", username: "some-user", secrets: {}, }) @@ -332,10 +340,24 @@ describe("ProjectConfigContext", () => { delete process.env.TEST_VARIABLE }) + it("should resolve the current git branch", () => { + const c = new ProjectConfigContext({ + projectName: "some-project", + artifactsPath: "/tmp", + branch: "main", + username: "some-user", + secrets: {}, + }) + expect(c.resolve({ key: ["git", "branch"], nodePath: [], opts: {} })).to.eql({ + resolved: "main", + }) + }) + it("should resolve secrets", () => { const c = new ProjectConfigContext({ projectName: "some-project", artifactsPath: "/tmp", + branch: "main", username: "some-user", secrets: { foo: "banana" }, }) @@ -348,6 +370,7 @@ describe("ProjectConfigContext", () => { const c = new ProjectConfigContext({ projectName: "some-project", artifactsPath: "/tmp", + branch: "main", username: "some-user", secrets: {}, }) @@ -364,6 +387,7 @@ describe("ProjectConfigContext", () => { const c = new ProjectConfigContext({ projectName: "some-project", artifactsPath: "/tmp", + branch: "main", username: "some-user", secrets: {}, }) @@ -432,6 +456,12 @@ describe("ModuleConfigContext", () => { }) }) + it("should resolve the current git branch", () => { + expect(c.resolve({ key: ["git", "branch"], nodePath: [], opts: {} })).to.eql({ + resolved: currentBranch, + }) + }) + it("should resolve the path of a module", async () => { const path = join(garden.projectRoot, "module-a") expect(c.resolve({ key: ["modules", "module-a", "path"], nodePath: [], opts: {} })).to.eql({ resolved: path }) @@ -574,6 +604,12 @@ describe("WorkflowConfigContext", () => { }) }) + it("should resolve the current git branch", () => { + expect(c.resolve({ key: ["git", "branch"], nodePath: [], opts: {} })).to.eql({ + resolved: currentBranch, + }) + }) + it("should resolve the environment config", async () => { expect(c.resolve({ key: ["environment", "name"], nodePath: [], opts: {} })).to.eql({ resolved: garden.environmentName, diff --git a/core/test/unit/src/config/project.ts b/core/test/unit/src/config/project.ts index 68546da1d4..e35cf41bea 100644 --- a/core/test/unit/src/config/project.ts +++ b/core/test/unit/src/config/project.ts @@ -50,6 +50,7 @@ describe("resolveProjectConfig", () => { defaultEnvironment, config, artifactsPath: "/tmp", + branch: "main", username: "some-user", secrets: {}, }) @@ -87,6 +88,7 @@ describe("resolveProjectConfig", () => { defaultEnvironment, config, artifactsPath: "/tmp", + branch: "main", username: "some-user", secrets: {}, }) @@ -139,6 +141,7 @@ describe("resolveProjectConfig", () => { defaultEnvironment, config, artifactsPath: "/tmp", + branch: "main", username: "some-user", secrets: { foo: "banana" }, }) @@ -207,7 +210,14 @@ describe("resolveProjectConfig", () => { process.env.TEST_ENV_VAR_B = "boo" expect( - resolveProjectConfig({ defaultEnvironment, config, artifactsPath: "/tmp", username: "some-user", secrets: {} }) + resolveProjectConfig({ + defaultEnvironment, + config, + artifactsPath: "/tmp", + branch: "main", + username: "some-user", + secrets: {}, + }) ).to.eql({ ...config, environments: [ @@ -267,6 +277,7 @@ describe("resolveProjectConfig", () => { defaultEnvironment, config, artifactsPath: "/tmp", + branch: "main", username: "some-user", secrets: {}, }) @@ -290,7 +301,14 @@ describe("resolveProjectConfig", () => { } expect( - resolveProjectConfig({ defaultEnvironment, config, artifactsPath: "/tmp", username: "some-user", secrets: {} }) + resolveProjectConfig({ + defaultEnvironment, + config, + artifactsPath: "/tmp", + branch: "main", + username: "some-user", + secrets: {}, + }) ).to.eql({ ...config, defaultEnvironment: "local", @@ -316,7 +334,14 @@ describe("resolveProjectConfig", () => { } expect( - resolveProjectConfig({ defaultEnvironment, config, artifactsPath: "/tmp", username: "some-user", secrets: {} }) + resolveProjectConfig({ + defaultEnvironment, + config, + artifactsPath: "/tmp", + branch: "main", + username: "some-user", + secrets: {}, + }) ).to.eql({ ...config, defaultEnvironment: "local", @@ -361,7 +386,14 @@ describe("resolveProjectConfig", () => { } expect( - resolveProjectConfig({ defaultEnvironment, config, artifactsPath: "/tmp", username: "some-user", secrets: {} }) + resolveProjectConfig({ + defaultEnvironment, + config, + artifactsPath: "/tmp", + branch: "main", + username: "some-user", + secrets: {}, + }) ).to.eql({ ...config, environments: [ @@ -429,7 +461,14 @@ describe("resolveProjectConfig", () => { } expect( - resolveProjectConfig({ defaultEnvironment, config, artifactsPath: "/tmp", username: "some-user", secrets: {} }) + resolveProjectConfig({ + defaultEnvironment, + config, + artifactsPath: "/tmp", + branch: "main", + username: "some-user", + secrets: {}, + }) ).to.eql({ ...config, environments: [ @@ -491,7 +530,15 @@ describe("pickEnvironment", () => { } await expectError( - () => pickEnvironment({ projectConfig: config, envString: "foo", artifactsPath, username, secrets: {} }), + () => + pickEnvironment({ + projectConfig: config, + envString: "foo", + artifactsPath, + branch: "main", + username, + secrets: {}, + }), "parameter" ) }) @@ -510,7 +557,14 @@ describe("pickEnvironment", () => { } expect( - await pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username, secrets: {} }) + await pickEnvironment({ + projectConfig: config, + envString: "default", + artifactsPath, + branch: "main", + username, + secrets: {}, + }) ).to.eql({ environmentName: "default", namespace: "default", @@ -546,7 +600,14 @@ describe("pickEnvironment", () => { } expect( - await pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username, secrets: {} }) + await pickEnvironment({ + projectConfig: config, + envString: "default", + artifactsPath, + branch: "main", + username, + secrets: {}, + }) ).to.eql({ environmentName: "default", namespace: "default", @@ -581,7 +642,14 @@ describe("pickEnvironment", () => { } expect( - await pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username, secrets: {} }) + await pickEnvironment({ + projectConfig: config, + envString: "default", + artifactsPath, + branch: "main", + username, + secrets: {}, + }) ).to.eql({ environmentName: "default", namespace: "default", @@ -635,6 +703,7 @@ describe("pickEnvironment", () => { projectConfig: config, envString: "default", artifactsPath, + branch: "main", username, secrets: {}, }) @@ -687,6 +756,7 @@ describe("pickEnvironment", () => { projectConfig: config, envString: "default", artifactsPath, + branch: "main", username, secrets: {}, }) @@ -733,6 +803,7 @@ describe("pickEnvironment", () => { projectConfig: config, envString: "default", artifactsPath, + branch: "main", username, secrets: {}, }) @@ -780,6 +851,7 @@ describe("pickEnvironment", () => { projectConfig: config, envString: "default", artifactsPath, + branch: "main", username, secrets: {}, }) @@ -827,6 +899,7 @@ describe("pickEnvironment", () => { projectConfig: config, envString: "default", artifactsPath, + branch: "main", username, secrets: {}, }) @@ -885,6 +958,7 @@ describe("pickEnvironment", () => { projectConfig: config, envString: "default", artifactsPath, + branch: "main", username, secrets: {}, }) @@ -944,6 +1018,7 @@ describe("pickEnvironment", () => { projectConfig: config, envString: "default", artifactsPath, + branch: "main", username, secrets: {}, }) @@ -975,6 +1050,7 @@ describe("pickEnvironment", () => { projectConfig: config, envString: "default", artifactsPath, + branch: "main", username, secrets: { foo: "banana" }, }) @@ -1001,7 +1077,14 @@ describe("pickEnvironment", () => { variables: {}, } - await pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username, secrets: {} }) + await pickEnvironment({ + projectConfig: config, + envString: "default", + artifactsPath, + branch: "main", + username, + secrets: {}, + }) }) it("should pass through template strings in the providers field on environments", async () => { @@ -1028,6 +1111,7 @@ describe("pickEnvironment", () => { projectConfig: config, envString: "default", artifactsPath, + branch: "main", username, secrets: {}, }) @@ -1052,6 +1136,7 @@ describe("pickEnvironment", () => { projectConfig: config, envString: "default", artifactsPath, + branch: "main", username, secrets: {}, }) @@ -1110,6 +1195,7 @@ describe("pickEnvironment", () => { projectConfig: config, envString: "default", artifactsPath, + branch: "main", username, secrets: {}, }) @@ -1145,7 +1231,15 @@ describe("pickEnvironment", () => { } await expectError( - () => pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username, secrets: {} }), + () => + pickEnvironment({ + projectConfig: config, + envString: "default", + artifactsPath, + branch: "main", + username, + secrets: {}, + }), (err) => expect(stripAnsi(err.message)).to.equal( "Error validating environment default: key .defaultNamespace must be a string" @@ -1174,7 +1268,15 @@ describe("pickEnvironment", () => { } await expectError( - () => pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username, secrets: {} }), + () => + pickEnvironment({ + projectConfig: config, + envString: "default", + artifactsPath, + branch: "main", + username, + secrets: {}, + }), (err) => expect(err.message).to.equal("Could not find varfile at path 'foo.env'") ) }) @@ -1200,7 +1302,15 @@ describe("pickEnvironment", () => { } await expectError( - () => pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username, secrets: {} }), + () => + pickEnvironment({ + projectConfig: config, + envString: "default", + artifactsPath, + branch: "main", + username, + secrets: {}, + }), (err) => expect(err.message).to.equal("Could not find varfile at path 'foo.env'") ) }) @@ -1219,7 +1329,14 @@ describe("pickEnvironment", () => { } expect( - await pickEnvironment({ projectConfig: config, envString: "foo.default", artifactsPath, username, secrets: {} }) + await pickEnvironment({ + projectConfig: config, + envString: "foo.default", + artifactsPath, + branch: "main", + username, + secrets: {}, + }) ).to.eql({ environmentName: "default", namespace: "foo", @@ -1243,7 +1360,14 @@ describe("pickEnvironment", () => { } expect( - await pickEnvironment({ projectConfig: config, envString: "foo.default", artifactsPath, username, secrets: {} }) + await pickEnvironment({ + projectConfig: config, + envString: "foo.default", + artifactsPath, + branch: "main", + username, + secrets: {}, + }) ).to.eql({ environmentName: "default", namespace: "foo", @@ -1267,7 +1391,14 @@ describe("pickEnvironment", () => { } expect( - await pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username, secrets: {} }) + await pickEnvironment({ + projectConfig: config, + envString: "default", + artifactsPath, + username, + branch: "main", + secrets: {}, + }) ).to.eql({ environmentName: "default", namespace: "default", @@ -1291,7 +1422,15 @@ describe("pickEnvironment", () => { } await expectError( - () => pickEnvironment({ projectConfig: config, envString: "$.%", artifactsPath, username, secrets: {} }), + () => + pickEnvironment({ + projectConfig: config, + envString: "$.%", + artifactsPath, + branch: "main", + username, + secrets: {}, + }), (err) => expect(err.message).to.equal( "Invalid environment specified ($.%): must be a valid environment name or ." @@ -1313,7 +1452,15 @@ describe("pickEnvironment", () => { } await expectError( - () => pickEnvironment({ projectConfig: config, envString: "default", artifactsPath, username, secrets: {} }), + () => + pickEnvironment({ + projectConfig: config, + envString: "default", + artifactsPath, + branch: "main", + username, + secrets: {}, + }), (err) => expect(stripAnsi(err.message)).to.equal( "Environment default has defaultNamespace set to null, and no explicit namespace was specified. Please either set a defaultNamespace or explicitly set a namespace at runtime (e.g. --env=some-namespace.default)." diff --git a/core/test/unit/src/garden.ts b/core/test/unit/src/garden.ts index 19918d4a0e..33f949d7b6 100644 --- a/core/test/unit/src/garden.ts +++ b/core/test/unit/src/garden.ts @@ -1514,7 +1514,7 @@ describe("Garden", () => { (err) => { expect(err.message).to.equal("Failed resolving one or more providers:\n" + "- test") expect(stripAnsi(err.detail.messages[0])).to.equal( - "- test: Invalid template string ${bla.ble}: Could not find key bla. Available keys: environment, local, project, providers, secrets, var and variables." + "- test: Invalid template string ${bla.ble}: Could not find key bla. Available keys: environment, git, local, project, providers, secrets, var and variables." ) } ) diff --git a/core/test/unit/src/vcs/git.ts b/core/test/unit/src/vcs/git.ts index bf62f10914..ac7a9ede0d 100644 --- a/core/test/unit/src/vcs/git.ts +++ b/core/test/unit/src/vcs/git.ts @@ -97,6 +97,31 @@ describe("GitHandler", () => { }) }) + describe("getBranchName", () => { + it("should return undefined with no commits in repo", async () => { + const path = tmpPath + expect(await handler.getBranchName(log, path)).to.equal(undefined) + }) + + it("should return the current branch name when there are commits in the repo", async () => { + const path = tmpPath + await commit("init", tmpPath) + expect(await handler.getBranchName(log, path)).to.equal("master") + }) + + it("should throw a nice error when given a path outside of a repo", async () => { + await expectError( + () => handler.getBranchName(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/core/test/unit/src/vcs/vcs.ts b/core/test/unit/src/vcs/vcs.ts index 1bcdc69736..874342565c 100644 --- a/core/test/unit/src/vcs/vcs.ts +++ b/core/test/unit/src/vcs/vcs.ts @@ -46,6 +46,10 @@ class TestVcsHandler extends VcsHandler { return undefined } + async getBranchName() { + return "main" + } + async getTreeVersion(log: LogEntry, projectName: string, moduleConfig: ModuleConfig) { return this.testVersions[moduleConfig.path] || super.getTreeVersion(log, projectName, moduleConfig) } diff --git a/docs/reference/template-strings.md b/docs/reference/template-strings.md index b0f5f60499..180a427ed9 100644 --- a/docs/reference/template-strings.md +++ b/docs/reference/template-strings.md @@ -102,6 +102,36 @@ Example: my-variable: ${project.name} ``` +### `${git.*}` + +Information about the current state of the project's local git repository. + +| Type | +| -------- | +| `object` | + +### `${git.branch}` + +The current Git branch, if available. Resolves to an empty string if HEAD is in a detached state +(e.g. when rebasing), or if the repository has no commits. + +When using remote sources, the branch used is that of the project/top-level repository (the one that contains +the project configuration). + +The branch is computed at the start of the Garden command's execution, and is not updated if the current +branch changes during the command's execution (which could happen, for example, when using watch-mode +commands). + +| Type | +| -------- | +| `string` | + +Example: + +```yaml +my-variable: ${git.branch} +``` + ## Environment configuration context @@ -196,6 +226,36 @@ Example: my-variable: ${project.name} ``` +### `${git.*}` + +Information about the current state of the project's local git repository. + +| Type | +| -------- | +| `object` | + +### `${git.branch}` + +The current Git branch, if available. Resolves to an empty string if HEAD is in a detached state +(e.g. when rebasing), or if the repository has no commits. + +When using remote sources, the branch used is that of the project/top-level repository (the one that contains +the project configuration). + +The branch is computed at the start of the Garden command's execution, and is not updated if the current +branch changes during the command's execution (which could happen, for example, when using watch-mode +commands). + +| Type | +| -------- | +| `string` | + +Example: + +```yaml +my-variable: ${git.branch} +``` + ### `${variables.*}` A map of all variables defined in the project configuration. @@ -322,6 +382,36 @@ Example: my-variable: ${project.name} ``` +### `${git.*}` + +Information about the current state of the project's local git repository. + +| Type | +| -------- | +| `object` | + +### `${git.branch}` + +The current Git branch, if available. Resolves to an empty string if HEAD is in a detached state +(e.g. when rebasing), or if the repository has no commits. + +When using remote sources, the branch used is that of the project/top-level repository (the one that contains +the project configuration). + +The branch is computed at the start of the Garden command's execution, and is not updated if the current +branch changes during the command's execution (which could happen, for example, when using watch-mode +commands). + +| Type | +| -------- | +| `string` | + +Example: + +```yaml +my-variable: ${git.branch} +``` + ### `${variables.*}` A map of all variables defined in the project configuration, including environment-specific variables. @@ -541,6 +631,36 @@ Example: my-variable: ${project.name} ``` +### `${git.*}` + +Information about the current state of the project's local git repository. + +| Type | +| -------- | +| `object` | + +### `${git.branch}` + +The current Git branch, if available. Resolves to an empty string if HEAD is in a detached state +(e.g. when rebasing), or if the repository has no commits. + +When using remote sources, the branch used is that of the project/top-level repository (the one that contains +the project configuration). + +The branch is computed at the start of the Garden command's execution, and is not updated if the current +branch changes during the command's execution (which could happen, for example, when using watch-mode +commands). + +| Type | +| -------- | +| `string` | + +Example: + +```yaml +my-variable: ${git.branch} +``` + ### `${variables.*}` A map of all variables defined in the project configuration, including environment-specific variables. @@ -928,6 +1048,36 @@ Example: my-variable: ${project.name} ``` +### `${git.*}` + +Information about the current state of the project's local git repository. + +| Type | +| -------- | +| `object` | + +### `${git.branch}` + +The current Git branch, if available. Resolves to an empty string if HEAD is in a detached state +(e.g. when rebasing), or if the repository has no commits. + +When using remote sources, the branch used is that of the project/top-level repository (the one that contains +the project configuration). + +The branch is computed at the start of the Garden command's execution, and is not updated if the current +branch changes during the command's execution (which could happen, for example, when using watch-mode +commands). + +| Type | +| -------- | +| `string` | + +Example: + +```yaml +my-variable: ${git.branch} +``` + ### `${variables.*}` A map of all variables defined in the project configuration, including environment-specific variables. @@ -1312,6 +1462,36 @@ Example: my-variable: ${project.name} ``` +### `${git.*}` + +Information about the current state of the project's local git repository. + +| Type | +| -------- | +| `object` | + +### `${git.branch}` + +The current Git branch, if available. Resolves to an empty string if HEAD is in a detached state +(e.g. when rebasing), or if the repository has no commits. + +When using remote sources, the branch used is that of the project/top-level repository (the one that contains +the project configuration). + +The branch is computed at the start of the Garden command's execution, and is not updated if the current +branch changes during the command's execution (which could happen, for example, when using watch-mode +commands). + +| Type | +| -------- | +| `string` | + +Example: + +```yaml +my-variable: ${git.branch} +``` + ### `${variables.*}` A map of all variables defined in the project configuration, including environment-specific variables.