Skip to content

Errors reference

Every error this module produces, what causes it, and what to do about it. Messages below are quoted as the module emits them.

Errors are created and wrapped with cockroachdb/errors, so match with errors.Is and errors.As, never by comparing strings. Three errors carry a hint, retrievable with errors.FlattenHints(err).

Sentinel errors

Nine exported sentinels. All four are returned wrapped, so err == repo.ErrNoWorktree will not match; errors.Is(err, repo.ErrNoWorktree) will.

Sentinel Message
repo.ErrNoRepository repository not initialised; call Open, Clone, or SetRepo first
repo.ErrNoWorktree worktree not initialised; call Open, Clone, or SetTree first
repo.ErrAlreadyRepository path is already inside a git repository
repo.ErrMissingGitDir git directory is missing or unusable
repo.ErrTagAlreadyExists tag already exists
repo.ErrInvalidCommit commit must be a full 40-character SHA
repo.ErrMissingTagger an explicit tagger name and email are required
repo.ErrMissingTagMessage an annotated tag requires a message
repo.ErrMissingTagField missing required field

Which method returns which sentinel

Every method has a defined behaviour when called before a repository is open. None of them panics. A mis-sequenced call is an error value you can handle.

Method Before Open* / Clone / InitLocal
Commit, Checkout, CheckoutCommit, AddAll, WorkFS, WithWorkFS, WithTree ErrNoWorktree
Push, WalkTree, FileExists, DirectoryExists, GetFile, CreateRemote, Remote, WithRepo ErrNoRepository
CreateBranch ErrNoRepository, because it checks the repository first, then the worktree
CreateTag ErrNoRepository. It needs no worktree: a tag names a commit, not a checkout
AddToFS No sentinel. It reads no repository state; it copies the *object.File you hand it into the afero.Fs you hand it.
SourceIs, SetSource, GetAuth, SetKey, SetBasicAuth, SetRepo, SetTree No error return; safe at any time

SourceIs(repo.SourceUnknown) is true on a freshly constructed repository and stays true until an Open*/Clone/InitLocal call sets SourceLocal or SourceMemory.

path is already inside a git repository

Returned by InitLocal, wrapped with the location:

/tmp/project/nested: path is already inside a git repository

InitLocal refuses to act on an existing repository so a scaffolding caller can tell "newly initialised" from "already tracked". The probe walks upward, matching git's own discovery semantics, so a subdirectory of a repository is also refused.

Match with errors.Is(err, repo.ErrAlreadyRepository). To ask the question without attempting anything, call repo.DiscoverRepository(path), a read-only probe that returns (true, nil) for the same input and creates nothing.

OpenLocal deliberately does not return this error: it opens an existing repository or initialises a new one, conflating the two.

no <FORGE> token available for private repository

Returned by NewRepo when Private: true, token authentication is being attempted, and no token resolves. The forge name is upper-cased into the message, and a hint is attached:

no GITLAB token available for private repository
HINT: Set GITLAB_TOKEN or configure gitlab.auth.env in your config to enable git operations

Fix it by supplying a working Token, or by dropping Private if the repository is in fact public. Note that Private: true on its own produces no error at all. See Private.

The hint names a config layout this module does not define

gitlab.auth.env is the shape phpboyscout tools use in their own config files. This module reads no configuration, so treat the hint as a suggestion aimed at the host application's users, not a key you can set here.

failed to get SSH key

Returned by NewRepo when SSH.Configured and SSH.HasKey are set, SSH.Path names something, and the key cannot be loaded. The wrapped cause names the real problem:

Wrapped cause Meaning
failed to get SSH key: stat /path/to/key: no such file or directory Nothing at SSH.Path. Check for an unexpanded ~, which neither this module nor KeyPath expands. The inner text is whatever Settings.FS reports, so an in-memory FS phrases it differently.
could not open SSH key at '/path/to/key': path is a directory SSH.Path points at a directory.
failed to get SSH key: ssh: no key found The file exists but is not a private key.
SSH key '/path/to/key' is passphrase-protected See below.

Keys are read through Settings.FS, so a test can supply them from afero.NewMemMapFs().

SSH key '<path>' is passphrase-protected

Carries a hint:

HINT: Prompt for the passphrase and retry with GetSSHKeyWithPassphrase, or load the key into ssh-agent

This module never blocks on a terminal prompt, so an encrypted key is reported rather than read. Detect it with errors.As against *ssh.PassphraseMissingError from golang.org/x/crypto/ssh, prompt, and retry:

key, err := repo.GetSSHKeyWithPassphrase(path, fs, passphrase)
if err != nil {
    return err
}

r.SetKey(key)

failed to create SSH auth

Returned by NewRepo when the ssh-agent fallback cannot reach an agent. With SSH_AUTH_SOCK unset:

failed to create SSH auth: error creating SSH agent: "SSH agent requested but SSH_AUTH_SOCK not-specified"

The agent fallback is taken when SSH.Configured is true and any of the following holds: SSH.HasKey is false, SSH.Type is exactly agent, or SSH.Path is empty. Each of the first and third also emits a Warn on Settings.Logger.

unknown repo type: <value>

Returned by Open when repoType is neither repo.LocalRepo ("local") nor repo.InMemoryRepo ("inmemory"). The comparison lower-cases the value, so "LOCAL" and "InMemory" are accepted; the message echoes what you passed, uncased.

failed to open git repository at <path>

Returned by OpenLocal when go-git could not open the path and the reason was not git.ErrRepositoryNotExists. Only that one cause triggers the init fallback; every other open failure is returned wrapped.

That distinction is the point, and it is worth stating at its real size rather than larger. What is guaranteed is that a .git redirect whose target has gone is reported and never initialised over. See git directory is missing or unusable below.

A .git directory is a different matter. If go-git reports it as no repository (because HEAD is missing, say) the fallback initialises into it, which can complete a partially-present repository rather than refusing it. That is not an oversight: it is what git init does in the same directory, refs and all, and diverging from git there would cost more than the guarantee is worth.

Two causes worth naming. A linked worktree whose common directory has gone missing wraps go-git's ErrRepositoryIncomplete (repository's commondir path does not exist). A .git symlink wraps chroot boundary crossed whatever it points at, including a target inside the same working tree, which is a go-git limitation rather than anything wrong with the repository.

Both are reported here rather than triggering the fallback.

git directory is missing or unusable

Returned by OpenLocal, DiscoverRepository and InitLocal when the location holds a .git file naming a git directory that is not there, or that holds no repository. Match it with errors.Is(err, repo.ErrMissingGitDir).

/home/u/wt: .git names /home/u/project/.git/worktrees/wt: git directory is missing or unusable
HINT: The repository this worktree belongs to has moved or been pruned

The message names the git directory rather than only the location, because that is the part the caller did not supply and cannot see from where they are standing.

Three features put a .git file at a working directory and all three reach this error identically: a linked worktree, a --separate-git-dir checkout, and a submodule. The state arises when the repository behind one is moved, pruned or re-cloned while the working directory survives:

  • git worktree remove or git worktree prune, with the directory later restored from a backup or a sync client
  • the parent repository re-cloned, moved or deleted
  • a worktree directory copied to another machine on its own

Nothing is written. The working directory is left exactly as it was, which is the whole point of reporting rather than initialising: it holds real work whose repository has gone missing.

DiscoverRepository returns this error rather than (false, nil). A directory whose repository cannot be reached is not the same answer as "no repository here", and the probe is documented as the safe way to ask.

Two neighbouring shapes do not produce it, and keep their own errors:

Shape Error
.git file with no gitdir: prefix .git file has no gitdir: prefix, straight from go-git
linked worktree whose commondir target is missing ErrRepositoryIncomplete, see above

failed to initialise git repository / failed to initialize Git repository

Two spellings, from two different methods:

Message Method
failed to initialise git repository InitLocal
failed to initialize Git repository OpenLocal, on the init-if-absent fallback

Both wrap go-git's PlainInitWithOptions failure, typically an unwritable parent directory.

failed to create target directory

Returned by Clone when it cannot create targetPath. Clone creates the directory with os.MkdirAll at mode 0755 before contacting the remote, on the operating-system filesystem regardless of Settings.FS.

failed to create target directory: mkdir /tmp/x/afile: not a directory

That example is a path whose parent component is a regular file.

failed to clone repository

Returned by Clone when go-git's clone fails. The wrapped cause is go-git's:

Wrapped cause Meaning
repository not found Bad URL, or a private repository reached without usable credentials
repository already exists targetPath already contains a git repository
authentication required The remote wants credentials and none were configured
Get "<url>/info/refs?service=git-upload-pack": context canceled The ctx you passed ended before the fetch finished

A failed clone leaves the target directory behind, because Clone creates it first. Remove it yourself before retrying, or the retry fails with repository already exists once a partial repository is on disk.

failed to get HEAD reference: reference not found

Returned by every TreeReader method (WalkTree, FileExists, DirectoryExists and GetFile) when the repository has no commits yet.

This is the usual surprise after OpenLocal or InitLocal on an empty directory: HEAD is a symbolic reference to an unborn refs/heads/main, so there is no commit to resolve a tree from. It is not a corruption and not a missing-repository error; the sentinel checks passed.

Make a commit first, or check with DiscoverRepository and branch on it. The tree readers read the HEAD commit's tree, never the staging area or the working directory, so a file you have written but not committed also reports as absent.

The sibling messages failed to get HEAD commit and failed to get commit tree come from the same resolution path, further along.

failed to get file from git: file not found

Returned by GetFile for a path not present in the HEAD tree.

FileExists does not return this: it treats object.ErrFileNotFound as (false, nil). Use FileExists to ask, GetFile to fetch. DirectoryExists likewise returns (false, nil) for an absent directory, and (true, nil) for "" or "/", which name the tree root.

Errors that come straight from go-git

These are not wrapped by this module, so they arrive with go-git's own wording.

Message From Cause
author field is required Commit No commit identity. See below.
reference not found Checkout, CheckoutCommit The branch or commit does not exist in this repository
remote not found Push, CreateBranch No remote is configured. See below.
already up-to-date CreateBranch Not returned. CreateBranch swallows git.NoErrAlreadyUpToDate
Get "<url>/info/refs?service=git-upload-pack": context canceled Clone, OpenInMemory The context was cancelled mid-fetch

author field is required

Commit(ctx, msg, nil) passes empty *git.CommitOptions to go-git, which fills the author from git configuration: user.name and user.email, resolved across the system, global and repository scopes. With none set anywhere, go-git refuses.

This bites hardest in containers and CI runners, where $HOME holds no .gitconfig. It has nothing to do with Settings; this module supplies no identity of its own. Set one explicitly instead of relying on ambient configuration:

hash, err := r.Commit(ctx, "docs: add readme", &git.CommitOptions{
    Author: &object.Signature{
        Name:  "Release Bot",
        Email: "bot@example.com",
        When:  time.Now(),
    },
})

remote not found

Push needs a remote, and so does CreateBranch, but only in one case.

CreateBranch(ctx, name) checks whether the branch already exists. For a branch that is new, it creates and checks it out, and touches no remote. For a branch that already exists, on a non-in-memory repository, it then pulls to bring the branch up to date, and that pull fails with remote not found if the repository has no remote.

So calling CreateBranch twice with the same name on a purely local repository succeeds the first time and fails the second. Add a remote with CreateRemote, or use Checkout(plumbing.NewBranchReferenceName(name)) to move to a branch that already exists.

The pull is skipped entirely when SourceIs(repo.SourceMemory) is true.

Tagging

CreateTag refuses more than it accepts, and each refusal removes a way to publish a tag that means something other than what was asked for.

commit must be a full 40-character SHA

ErrInvalidCommit. The commit is validated, never resolved: a caller reaching CreateTag has already established which commit to tag, and re-resolving a name would reopen the race that check closed.

An abbreviation is refused rather than expanded, and the reason is sharper than it looks. plumbing.NewHash reports nothing for a bad value, and for the likeliest mistake its result is the dangerous one:

you pass NewHash gives IsZero()
main 0000…0000 true
0173e42 0173e40000000000000000000000000000000000 false

An abbreviated SHA — what a person pastes, and what every forge UI shows — is silently zero-padded into a different, valid-looking hash pointing at nothing. A guard written as if hash.IsZero() lets it through.

an explicit tagger name and email are required

ErrMissingTagger. go-git reads the name and email from the machine's git configuration when the tagger is nil, so the same release tagged from two machines is attributed to two different people, and in CI to whatever identity the runner image carries.

an annotated tag requires a message

ErrMissingTagMessage. This module creates annotated tags only. A lightweight tag is a bare reference that records neither who made it nor when, which is the wrong shape for the one artefact of a release that is permanent and public.

tag already exists

ErrTagAlreadyExists. A published tag has been fetched by consumers, and moving it means two artefacts answer to one version. There is no force option; a caller that genuinely needs one can reach WithRepo.