Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(git): fix confusing error messages on exit code 128 #5439

Merged
merged 8 commits into from
Nov 20, 2023
44 changes: 24 additions & 20 deletions core/src/vcs/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ import { performance } from "perf_hooks"
import { isAbsolute, join, posix, relative, resolve } from "path"
import { isString } from "lodash-es"
import fsExtra from "fs-extra"

const { createReadStream, ensureDir, lstat, pathExists, readlink, realpath, stat } = fsExtra
import { PassThrough } from "stream"
import type { GetFilesParams, RemoteSourceParams, VcsFile, VcsInfo, VcsHandlerParams } from "./vcs.js"
import type { GetFilesParams, RemoteSourceParams, VcsFile, VcsHandlerParams, VcsInfo } from "./vcs.js"
import { VcsHandler } from "./vcs.js"
import type { GardenError } from "../exceptions.js"
import { ChildProcessError, ConfigurationError, isErrnoException, RuntimeError } from "../exceptions.js"
import { getStatsType, joinWithPosix, matchPath } from "../util/fs.js"
import { dedent, deline, splitLast } from "../util/string.js"
Expand All @@ -34,6 +33,8 @@ import { execa } from "execa"
import hasha from "hasha"
import { styles } from "../logger/styles.js"

const { createReadStream, ensureDir, lstat, pathExists, readlink, realpath, stat } = fsExtra

const submoduleErrorSuggestion = `Perhaps you need to run ${styles.underline(`git submodule update --recursive`)}?`

interface GitEntry extends VcsFile {
Expand Down Expand Up @@ -133,21 +134,7 @@ export class GitHandler extends VcsHandler {
if (!(err instanceof ChildProcessError)) {
throw err
}
if (err.details.code === 128 && err.details.stderr.toLowerCase().includes("fatal: unsafe repository")) {
// Throw nice error when we detect that we're not in a repo root
throw new RuntimeError({
message:
err.details.stderr +
`\nIt looks like you're using Git 2.36.0 or newer and the repo directory containing "${path}" is owned by someone else. If this is intentional you can run "git config --global --add safe.directory '<repo root>'" and try again.`,
})
} else if (err.details.code === 128) {
// Throw nice error when we detect that we're not in a repo root
throw new RuntimeError({
message: notInRepoRootErrorMessage(path),
})
} else {
throw err
}
throw explainGitError(err, path)
}
})
}
Expand Down Expand Up @@ -690,11 +677,28 @@ export class GitHandler extends VcsHandler {
}
}

const notInRepoRootErrorMessage = (path: string) => deline`
function gitErrorContains(err: ChildProcessError, substring: string): boolean {
return err.details.stderr.toLowerCase().includes(substring.toLowerCase())
}

export function explainGitError(err: ChildProcessError, path: string): GardenError {
// handle some errors with exit codes 128 in a specific manner
if (err.details.code === 128) {
if (gitErrorContains(err, "fatal: not a git repository")) {
// Throw nice error when we detect that we're not in a repo root
return new RuntimeError({
message: 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.
`
`,
})
}
}

// otherwise just re-throw the original error
return err
}

/**
* Given a list of POSIX-style globs/paths and a `basePath`, find paths that point to a directory and append `**\/*`
Expand Down
75 changes: 70 additions & 5 deletions core/test/unit/src/vcs/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@ import { execa } from "execa"
import { expect } from "chai"
import tmp from "tmp-promise"
import fsExtra from "fs-extra"

const { createFile, ensureSymlink, lstat, mkdir, mkdirp, realpath, remove, symlink, writeFile } = fsExtra
import { basename, join, relative, resolve } from "path"

import type { TestGarden } from "../../../helpers.js"
import { expectError, makeTestGardenA } from "../../../helpers.js"
import type { GitCli } from "../../../../src/vcs/git.js"
import { getCommitIdFromRefList, GitHandler, parseGitUrl } from "../../../../src/vcs/git.js"
import { explainGitError, getCommitIdFromRefList, GitHandler, parseGitUrl } from "../../../../src/vcs/git.js"
import type { Log } from "../../../../src/logger/log-entry.js"
import { hashRepoUrl } from "../../../../src/util/ext-source-util.js"
import { deline } from "../../../../src/util/string.js"
import { dedent, deline } from "../../../../src/util/string.js"
import { uuidv4 } from "../../../../src/util/random.js"
import type { VcsHandlerParams } from "../../../../src/vcs/vcs.js"
import { repoRoot } from "../../../../src/util/testing.js"
import { GardenError } from "../../../../src/exceptions.js"
import { ChildProcessError, GardenError, RuntimeError } from "../../../../src/exceptions.js"

const { createFile, ensureSymlink, lstat, mkdir, mkdirp, realpath, remove, symlink, writeFile } = fsExtra

// Overriding this to make sure any ignorefile name is respected
export const defaultIgnoreFilename = ".testignore"
Expand Down Expand Up @@ -988,4 +988,69 @@ describe("git", () => {
await expectError(() => parseGitUrl(url), "configuration")
})
})

describe("explainGitError", () => {
const path = "/tmp"

function getChildProcessError(exitCode: number, stderr: string): ChildProcessError {
return new ChildProcessError({
cmd: "git",
args: ["rev-parse", "--show-toplevel"],
code: exitCode,
output: stderr,
stderr,
stdout: "",
})
}

context("on error code 128", () => {
const exitCode = 128

it("should throw a nice error when given a path outside of a repo", async () => {
const stderr = "fatal: not a git repository (or any of the parent directories): .git"
const gitError = getChildProcessError(exitCode, stderr)

const explainedGitError = explainGitError(gitError, path)
expect(explainedGitError).to.be.instanceof(RuntimeError)
expect(explainedGitError.message).to.eql(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.
`)
})

it("should throw an original error with exit code 128 the rest cases", async () => {
const stderr = `fatal: another git error with exit code ${exitCode}`
const gitError = getChildProcessError(exitCode, stderr)

const explainedGitError = explainGitError(gitError, path)
expect(explainedGitError).to.be.instanceof(ChildProcessError)

const castedExplainedGitError = explainedGitError as ChildProcessError
expect(castedExplainedGitError.details.code).to.eql(exitCode)
expect(castedExplainedGitError.message).to.eql(dedent`
Command "git rev-parse --show-toplevel" failed with code ${exitCode}:

${stderr}
`)
})
})

it("should throw an original error when exit code is not 128", async () => {
const exitCode = -1
const stderr = "fatal: unexpected git error"
const gitError = getChildProcessError(exitCode, stderr)

const explainedGitError = explainGitError(gitError, path)
expect(explainedGitError).to.be.instanceof(ChildProcessError)

const castedExplainedGitError = explainedGitError as ChildProcessError
expect(castedExplainedGitError.details.code).to.eql(exitCode)
expect(castedExplainedGitError.message).to.eql(dedent`
Command "git rev-parse --show-toplevel" failed with code ${exitCode}:

${stderr}
`)
})
})
})