Skip to content

Settings reference

Settings is the entire configuration surface of this module. NewRepo and NewThreadSafeRepo take one, resolve authentication from it once, and never read a config file, an environment variable or a keychain of their own.

Every field is optional. repo.NewRepo(repo.Settings{}) succeeds and returns a repository that can clone and read public repositories unauthenticated.

Settings fields at a glance

Field Type Zero value behaviour
Forge string Treated as github
Private bool A missing token is not an error
AuthEnabled bool Token auth is only attempted when Token is non-nil
Token TokenSource (func() string) No token; clone/push are unauthenticated
SSH SSHSettings SSH auth is not selected
Logger Logger Diagnostics are discarded
FS afero.Fs afero.NewOsFs()
Progress io.Writer The server's sideband output is discarded

Forge: which username token auth sends

Forge selects the username git-over-HTTPS expects alongside a token. It never selects an API client; no forge SDK is involved.

Forge value Username sent
gitlab (repo.ForgeGitLab) oauth2
bitbucket (repo.ForgeBitbucket) x-token-auth
github, gitea, codeberg, direct, "", anything else x-access-token

The value is trimmed and lower-cased before it is matched, so "GitLab" and " gitlab " both select the GitLab convention.

An unrecognised value is not an error. Forge: "my-internal-forge" is accepted and falls back to the GitHub convention, which is what lets a self-hosted or brand-new forge work without a code change here.

"" and direct (repo.ForgeDirect) also fall back to GitHub. direct names a plain download source with no git remote, so it has no convention of its own.

Forge also appears in the private-repository error and in the hint attached to it. See no <FORGE> token available for private repository.

Private: when a missing token becomes an error

Private: true turns a missing token into an immediate, hinted failure from NewRepo rather than an unauthenticated clone that fails later with an opaque git message.

Private on its own does nothing. It is only consulted on the token-authentication path, and that path only runs when AuthEnabled is true or Token is non-nil:

// No error. Token auth is never attempted, so Private is never read.
r, err := repo.NewRepo(repo.Settings{Forge: repo.ForgeGitLab, Private: true})

// Errors: "no GITLAB token available for private repository"
r, err := repo.NewRepo(repo.Settings{Forge: repo.ForgeGitLab, Private: true, AuthEnabled: true})

If you want a missing credential reported, set AuthEnabled: true as well as Private: true, or pass a Token. A StaticToken("") counts: it is non-nil, and an empty result is treated as "no token available".

Private is also never read when SSH.Configured is true: SSH takes priority and the token path is not entered at all.

AuthEnabled: asking for token auth without supplying a token

AuthEnabled: true runs the token-authentication path even when Token is nil, so a missing credential is reported rather than silently skipped. It is logged at debug for a public repository, and returned as an error when Private is also true.

With AuthEnabled: false and Token: nil, NewRepo configures no authentication at all and GetAuth() returns nil.

Token: the lazily-resolved credential

Token is a TokenSource, which is func() string, not a resolved string. It is called at most once, and only on the code path that actually authenticates with a token.

// already in hand
Token: repo.StaticToken(os.Getenv("GITLAB_TOKEN")),

// resolved on demand; this closure is never called on the SSH path
Token: func() string { return keychain.Get("gitlab") },

Two values are treated identically as "no token available": a nil TokenSource, and one that returns "". Both leave GetAuth() nil for a public repository and produce the private-repository error when Private is true.

A repository configured for SSH never invokes Token. That is a guarantee, not an ordering accident: a token source may prompt the user to unlock an OS keychain, and an SSH clone must not trigger that prompt. The module's own tests install a source that panics if called to keep the guarantee enforceable.

FS: the filesystem used to read SSH key files, and nothing else

FS defaults to afero.NewOsFs() when nil.

FS is not a general filesystem abstraction for this module. It is consulted in exactly one place: reading an SSH private key from SSH.Path during NewRepo. It is a seam so a test can supply a key from afero.NewMemMapFs() without touching disk.

Everything else uses the real filesystem regardless of what FS holds:

  • Clone creates its target directory with os.MkdirAll (mode 0755).
  • OpenLocal, InitLocal and DiscoverRepository go through go-git's own PlainOpen / PlainInit, which read and write the operating-system filesystem.
  • OpenInMemory uses go-git's memfs and memory.Storage, not FS.

Passing a MemMapFs will therefore not keep a Clone off disk. Use OpenInMemory for that.

Logger: where fallback decisions are reported

Logger is a two-method interface that *slog.Logger satisfies directly:

type Logger interface {
    Debug(msg string, keyvals ...any)
    Warn(msg string, keyvals ...any)
}

A nil Logger is tolerated and discards everything. It is worth wiring up, because these three decisions are reported only here and are otherwise invisible:

Level Message Emitted when
Warn No ssh.key subtree configured for forge, defaulting to ssh-agent SSH.Configured is true but SSH.HasKey is false
Warn No SSH key path configured for forge, defaulting to ssh-agent SSH.Path is empty, or cleans to .
Debug No credential configured for forge; using unauthenticated git access Token auth ran, no token resolved, and Private is false

Each carries one key/value pair: "forge" and the resolved forge name.

Logger reports decisions this module made. It does not carry anything the git server said. That is Progress.

Progress: the remote server's own output

Progress is an io.Writer receiving the remote's sideband stream during clone, pull and push: the lines real git renders as Receiving objects: 45% (450/1000) and remote: .... Nil discards them.

Progress: os.Stderr,

It is wired into every operation that talks to a remote: the clone inside OpenInMemory, the clone inside Clone, the pull inside CreateBranch, and Push.

Push backfills it from Settings the same way it backfills Auth, so setting other push options does not silently drop it. An explicit opts.Progress wins.

Leaving it nil loses information no logger can reconstruct: pre-receive hook rejection reasons and the "To create a merge request, visit …" URL arrive on this channel and nowhere else.

It is remote-controlled text

The bytes come from the server and may contain terminal control characters. Send it to a terminal or a buffer; do not pipe it unsanitised into a log aggregator.

SSHSettings fields

type SSHSettings struct {
    Configured bool
    HasKey     bool
    Type       string
    Path       string
}
Field Effect
Configured Selects the SSH path. When true, token authentication is never attempted and Token is never called.
HasKey False means the caller's SSH block exists but names no key (a bare github.ssh: true). Falls back to ssh-agent and warns.
Type The exact string agent selects ssh-agent. The comparison is case-sensitive: Agent and AGENT do not match and fall through to the key-path branch.
Path A resolved filesystem path to a private key, read through FS. Empty (or any value that filepath.Clean reduces to .) falls back to ssh-agent and warns.

The ssh-agent fallback builds its credential from go-git's ssh.DefaultAuthBuilder, which connects to the agent named by SSH_AUTH_SOCK. With that variable unset, NewRepo fails:

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

An unreadable key is fatal to NewRepo. See failed to get SSH key.

Which authentication path a Settings value selects

Read top to bottom; the first matching row wins.

SSH.Configured AuthEnabled Token Result
true SSH auth. Token is never called.
false true non-nil, non-empty Basic auth: forge username + token
false false non-nil, non-empty Basic auth: forge username + token
false true nil or empty, Private: false No auth; debug diagnostic
false true nil or empty, Private: true NewRepo returns an error
false false nil No auth configured; GetAuth() is nil

Authentication is resolved once, in NewRepo. To change it afterwards, call SetKey or SetBasicAuth on the repository.

KeyPath: environment fallback, applied by you

func KeyPath(path, env string) string

KeyPath applies the usual "explicit path, else named environment variable" precedence and returns "" when neither yields a value. Both results are passed through filepath.Clean.

settings.SSH.Path = repo.KeyPath(cfg.SSHKeyPath, "GITHUB_SSH_KEY")
path env Result
"/a/b/../c" anything "/a/c"
"" a set variable the variable's value, cleaned
"" an unset variable ""
"" "" ""

KeyPath is the only function in this module that reads the environment, and nothing in the module calls it. The caller invokes it from its own composition root, and TestEnvFootprint walks the package's AST to keep both halves of that true.

It does not expand ~: KeyPath("", "K") with K=~/x/../y returns ~/y, which is not a path any filesystem will resolve. Expand the home directory yourself before calling it.