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:
Clonecreates its target directory withos.MkdirAll(mode0755).OpenLocal,InitLocalandDiscoverRepositorygo through go-git's ownPlainOpen/PlainInit, which read and write the operating-system filesystem.OpenInMemoryuses go-git'smemfsandmemory.Storage, notFS.
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:
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.
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¶
| 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¶
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.
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.
Related¶
- Authenticate to a forge: the task-shaped version
- Errors reference: every error these settings can produce
- Why git needs no forge: why
Settingsis the whole input surface