repo

package module
v0.5.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 15, 2026 License: MIT Imports: 20 Imported by: 0

README

repo

Git repository operations for Go: clone, commit, worktrees and tree inspection over go-git, behind focused role interfaces, with in-memory and filesystem backends

Go Reference Pipeline Coverage phpboyscout Go toolkit

Part of the phpboyscout Go toolkit: small, framework-free Go modules extracted from go-tool-base. Docs: repo.go.phpboyscout.uk


Cloning a repository from GitHub should not put a GitHub SDK in your dependency graph. Git doesn't know what a forge is (it wants a URL and a credential), so this module doesn't either.

r, err := repo.NewRepo(repo.Settings{
	Forge: repo.ForgeGitHub,
	Token: repo.StaticToken(os.Getenv("GITHUB_TOKEN")),
	FS:    afero.NewOsFs(),
})

// Every network-touching method takes a context, so a hung remote is cancellable.
_, _, err = r.OpenInMemory(ctx, url, "main", repo.WithShallowClone(1))
file, err := r.GetFile("go.mod")

Design

  • No forge dependency. It authenticates to GitHub, GitLab, Bitbucket, Gitea and Codeberg while importing none of them: Forge is a string naming an authentication convention, not a client. An unrecognised name is accepted, so a self-hosted or brand new forge works without a code change. A depfootprint_test.go guard keeps every forge SDK out of the graph.
  • Depend on the narrowest role. RepoLike composes ten focused interfaces: TreeReader, Opener, Committer, Brancher and friends. A function that only reads the tree takes TreeReader, so its contract is honest and a two-method fake satisfies it. mocks/ ships twelve mocks, one for each of the eleven roles (Initializer is the eleventh, held outside RepoLike on purpose) and one for the composite. mocks_conformance_test.go fails to compile the moment a mock stops satisfying its role, so a regenerated mocks/ is part of an interface change rather than a follow-up.
  • In memory or on disk, same API. The in-memory backend is a real repository with no disk and no network. Fast, hermetic, nothing to clean up, and it makes an excellent test fixture precisely because there is no mock to drift from real git behaviour.
  • The worktree is an afero.Fs. Read and write a checked-out tree with ordinary afero code, live: no copy and no sync step, via aferobilly.
  • Settings is the whole input surface. This package reads no environment of its own; every input arrives through Settings. Construction is fully determined by its arguments, settings scope to one repository rather than the whole process, and tests never need t.Setenv (which would forbid t.Parallel()). envfootprint_test.go walks the AST of every non-test file and permits os.Getenv in exactly one function, which the package itself never calls.
  • Lazy credentials. A token is a TokenSource func, resolved only if the token path is taken, so an SSH-configured repository never triggers a keychain prompt. The test suite proves it with a source that panics if called.

Install

go get gitlab.com/phpboyscout/go/repo

Concurrency

go-git is not thread-safe: it mutates internal caches even on reads. ThreadSafeRepo serialises every operation through an exclusive mutex, reads included, which is why a sync.RWMutex would not be sound here:

r, err := repo.NewThreadSafeRepo(settings)

Operations are individually atomic. When a sequence has to be, use the WithRepo, WithTree or WithWorkFS callbacks. A WorkFS() handle stays safe after it escapes: every call on it, and on every file opened from it, re-locks the same mutex.

[!WARNING] Go mutexes are not reentrant. Never call a ThreadSafeRepo method from inside one of its own callbacks, and never retain the pointer a callback was handed.

Documentation

Full guides: repo.go.phpboyscout.uk. API reference: pkg.go.dev.

Worth knowing before you start:

License

See LICENSE.

Documentation

Overview

Package repo provides git repository operations (clone, checkout, commit, push, annotated tags, tree inspection) backed by go-git, behind the RepoLike abstraction with local-filesystem and in-memory storage backends.

RepoLike is a composite built from focused role interfaces: TreeReader, Opener, Authenticator, WorktreeController, Committer, SourceState, GitAccessor, Brancher, Tagger and WorktreeFS. Consumers should depend on the narrowest role that covers what they touch rather than the full composite. The named-remote operations CreateRemote/Remote remain concrete-only on *Repo and are not part of any role interface.

The init-only primitives form the Initializer role: DiscoverRepository (a read-only upward probe for an enclosing .git), Repo.InitLocal (init that refuses an existing repo with ErrAlreadyRepository), and Repo.AddAll (gitignore-aware staging). It is deliberately NOT embedded in RepoLike: init/discovery and ignore-aware staging are first-commit concerns a clone/checkout consumer never needs, so a scaffold-time caller depends on Initializer directly.

The package is a deliberately thin veneer over go-git: the role interfaces traffic in go-git's own concrete types (*git.Repository, *git.Worktree, *git.PushOptions, plumbing.Hash, …) by design rather than reinventing an abstraction over them. Every network-touching method (Opener.Open, Opener.OpenInMemory, Opener.Clone, Committer.Push and Brancher.CreateBranch) takes a context.Context as its first argument so a hung remote can be cancelled. This matters most on ThreadSafeRepo, which holds its mutex for a method's full duration: without a cancellable context a stalled clone or push would block every other caller indefinitely. The non-network methods on those same roles (Opener.OpenLocal, Committer.Commit) accept a context purely for signature symmetry.

Clone/push authentication is forge-aware without depending on a forge: NewRepo takes plain Settings naming the forge and supplying a lazy TokenSource, so the package resolves neither configuration nor credentials itself. SSH authentication takes priority when SSHSettings are configured, in which case the token source is never consulted. Missing credentials are non-fatal for public repositories; a private repository fails fast.

Index

Constants

View Source
const (
	ForgeGitHub    = "github"
	ForgeGitLab    = "gitlab"
	ForgeBitbucket = "bitbucket"
	ForgeGitea     = "gitea"
	ForgeCodeberg  = "codeberg"
	ForgeDirect    = "direct"
)

Forge identifiers understood by the git-authentication paths. They select the username git-over-HTTPS expects for token auth; any other value falls back to the GitHub convention.

These are plain strings rather than an enum so a caller can name a forge this package has never heard of, and so the package needs no dependency on a forge or release abstraction merely to identify one.

View Source
const (
	SourceUnknown = iota
	SourceMemory
	SourceLocal
)

Variables

View Source
var (
	// ErrNoRepository is returned by WithRepo when no *git.Repository has been set.
	ErrNoRepository = errors.NewSentinel("repo.no_repository", "repository not initialised; call Open, Clone, or SetRepo first")

	// ErrNoWorktree is returned by WithTree when no *git.Worktree has been set.
	ErrNoWorktree = errors.NewSentinel("repo.no_worktree", "worktree not initialised; call Open, Clone, or SetTree first")

	// ErrTagAlreadyExists reports a tag that is already present. This module
	// never moves one; see spec 0006 D5.
	ErrTagAlreadyExists = errors.NewSentinel("repo.tag_exists", "tag already exists")

	// ErrInvalidCommit reports a commit reference that is not a full SHA. An
	// abbreviation is refused rather than expanded, because expanding is
	// resolving. See spec 0006 D3.
	ErrInvalidCommit = errors.NewSentinel("repo.invalid_commit", "commit must be a full 40-character SHA")

	// ErrMissingTagger reports an absent or incomplete tagger identity. go-git
	// would otherwise read one from the machine's git configuration.
	ErrMissingTagger = errors.NewSentinel("repo.missing_tagger", "an explicit tagger name and email are required")

	// ErrMissingTagMessage reports an empty tag message. Tags created here are
	// annotated, so a message is required.
	ErrMissingTagMessage = errors.NewSentinel("repo.missing_tag_message", "an annotated tag requires a message")

	// ErrMissingTagField reports a required field absent from a TagRequest.
	ErrMissingTagField = errors.NewSentinel("repo.missing_tag_field", "missing required field")

	// ErrNoSparseDirectories reports WithSparseCheckout given no directory, or
	// one naming the whole tree. Applying either as a full checkout would spend
	// what the caller asked to save. See spec 0007 D5.
	ErrNoSparseDirectories = errors.NewSentinel("repo.no_sparse_directories", "sparse checkout requires at least one directory")

	// ErrNoSparseMatch reports a sparse set that matched no path in the tree.
	// Without it the first symptom is an empty commit refused three calls
	// later, which reads as "nothing to do". See spec 0007 D9.
	ErrNoSparseMatch = errors.NewSentinel("repo.no_sparse_match", "sparse checkout matched no path in the tree")
)
View Source
var ErrAlreadyRepository = errors.NewSentinel("repo.already_repository", "path is already inside a git repository")

ErrAlreadyRepository is returned by InitLocal when the target path is already inside a git repository. Unlike OpenLocal, which opens an existing repo or initialises a new one and conflates the two outcomes, InitLocal refuses to act on an existing repository so a caller (e.g. a scaffolding tool's first-commit step) can reliably distinguish "newly initialised" from "already tracked".

View Source
var ErrMissingGitDir = errors.NewSentinel("repo.missing_git_dir", "git directory is missing or unusable")

ErrMissingGitDir is returned when a location holds a .git file naming a git directory that is not there, or that holds no repository. That is the state a linked worktree, a --separate-git-dir checkout or a submodule lands in when the repository behind it is moved, pruned or re-cloned: the working directory survives with real work in it, and the administrative directory it points at does not.

All three features use the same .git redirect file, so all three produce this error. The working directory is never touched. The point of the sentinel is that a caller can tell "this directory is empty, initialise it" apart from "this directory holds work whose repository has gone missing, stop".

A .git *directory* is a different case and does not produce this: it is handed to go-git's init exactly as "git init" would hand it to git's. See docs/explanation/limitations.md.

Functions

func DiscoverRepository

func DiscoverRepository(location string) (found bool, err error)

DiscoverRepository reports whether location is inside a git repository, walking upward from location to find an enclosing .git directory (matching git's own discovery semantics). It is a read-only probe: it never creates or modifies a repository. A subdirectory of an existing repository reports true.

This is distinct from OpenLocal, which init-if-absent and cannot tell an opened existing repository apart from a freshly initialised one.

A location whose .git names a git directory that has gone returns ErrMissingGitDir rather than (false, nil): a repository that cannot be reached is not the same answer as no repository at all. InitLocal propagates that, so it refuses such a location rather than initialising over it.

func GetSSHKey

func GetSSHKey(filePath string, localfs afero.Fs) (*ssh.PublicKeys, error)

GetSSHKey loads an SSH private key from the filesystem for git authentication. If the key is passphrase-protected, the returned error wraps gossh.PassphraseMissingError; interactive callers (the CLI layer) should detect it with errors.As, prompt the user, and retry via GetSSHKeyWithPassphrase. This package never blocks on a TUI.

func GetSSHKeyWithPassphrase

func GetSSHKeyWithPassphrase(filePath string, localfs afero.Fs, passphrase string) (*ssh.PublicKeys, error)

GetSSHKeyWithPassphrase loads an SSH private key from the filesystem, decrypting it with the supplied passphrase when non-empty.

func KeyPath

func KeyPath(path, env string) string

KeyPath resolves an SSH key path from an explicit path with an environment-variable fallback, returning "" when neither yields one.

It is a helper for callers assembling SSHSettings, not something this package calls itself. Reading the environment stays the caller's decision, made in its composition root where the rest of its configuration is resolved:

settings.SSH.Path = repo.KeyPath(cfg.SSHKeyPath, "GITHUB_SSH_KEY")

Types

type Authenticator

type Authenticator interface {
	SetKey(*ssh.PublicKeys)
	SetBasicAuth(string, string)
	GetAuth() transport.AuthMethod
	SetRepo(*git.Repository)
}

Authenticator configures credentials and the underlying repository handle.

type Brancher

type Brancher interface {
	CreateBranch(context.Context, string) error
}

Brancher creates branches. CreateBranch takes a context.Context because it may pull from a remote for an already-existing branch.

type CloneOption

type CloneOption func(*CloneOptions)

CloneOption represents a function that configures clone options.

func WithNoTags

func WithNoTags() CloneOption

WithNoTags configures the clone to skip fetching tags.

func WithRecurseSubmodules

func WithRecurseSubmodules() CloneOption

WithRecurseSubmodules configures recursive submodule cloning.

func WithShallowClone

func WithShallowClone(depth int) CloneOption

WithShallowClone configures a shallow clone with the specified depth.

func WithSingleBranch

func WithSingleBranch(branch string) CloneOption

WithSingleBranch configures the clone to only fetch a single branch.

func WithSparseCheckout added in v0.5.0

func WithSparseCheckout(dirs ...string) CloneOption

WithSparseCheckout limits the working tree to the paths under the given directory prefixes. The clone fetches every object either way; what it skips is materialising the rest of the tree, which for an in-memory repository is the second copy of every file.

go-git matches by string prefix, so "data/releases" also keeps "data/releases-old/"; end a prefix with "/" for an exact directory. The working tree stays sparse for the life of the handle: a later Checkout does not widen it. See spec 0007.

type CloneOptions added in v0.5.0

type CloneOptions struct {
	git.CloneOptions

	// SparseCheckoutDirectories limits the working tree to the paths under
	// these prefixes. Nil means no sparse checkout; empty means one was asked
	// for with nothing in it, which is refused.
	SparseCheckoutDirectories []string
}

CloneOptions is what a CloneOption configures. It embeds go-git's CloneOptions, so a hand-written option still reaches every go-git field, and carries beside it what the module does after the clone. See spec 0007 D2.

type Committer

type Committer interface {
	Commit(context.Context, string, *git.CommitOptions) (plumbing.Hash, error)
	Push(context.Context, *git.PushOptions) error
}

Committer records and publishes changes. Push takes a context.Context so a hung remote can be cancelled; Commit accepts one for signature symmetry but performs no network I/O.

type GitAccessor

type GitAccessor interface {
	// WithRepo calls fn with the underlying *git.Repository.
	// Returns ErrNoRepository if the repository has not been initialised.
	WithRepo(func(*git.Repository) error) error

	// WithTree calls fn with the underlying *git.Worktree.
	// Returns ErrNoWorktree if the worktree has not been initialised.
	WithTree(func(*git.Worktree) error) error
}

GitAccessor exposes raw go-git escape hatches.

type Initializer

type Initializer interface {
	InitLocal(location, branch string) (*git.Repository, *git.Worktree, error)
	AddAll() error
}

Initializer is the init-only role: it initialises a fresh repository on an explicit default branch and stages the worktree honouring .gitignore. It is deliberately NOT embedded in RepoLike, because init/discovery and gitignore-aware staging are first-commit concerns a clone/checkout consumer never needs, so callers that need them (the generator's scaffold step) depend on this narrow role directly. Both *Repo and *ThreadSafeRepo satisfy it.

type Logger

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

Logger is the diagnostic logging seam. It is deliberately two methods wide, because this package reports what it did and what it skipped and nothing more, so a *slog.Logger satisfies it directly and no logging library is imposed on consumers.

A nil Logger is tolerated: diagnostics are simply discarded.

type Opener

Opener acquires a repository via local open, in-memory open, or clone. The network-touching methods (OpenInMemory and Clone, plus Open when it dispatches to them) take a context.Context so a hung remote can be cancelled; OpenLocal accepts one for signature symmetry but performs no network I/O.

type Repo

type Repo struct {
	// contains filtered or unexported fields
}

Repo implements RepoLike using go-git for local and in-memory repositories.

func NewRepo

func NewRepo(settings Settings, ops ...RepoOpt) (*Repo, error)

NewRepo creates a Repo with the given typed settings and options.

Clone/push authentication is resolved from the selected forge (github, gitlab, bitbucket, gitea, codeberg). When Settings.SSH.Configured is set, SSH auth is used and Settings.Token is never invoked; otherwise token auth is resolved lazily from Settings.Token. A missing credential is only an error when Settings.Private is true.

func (*Repo) AddAll

func (r *Repo) AddAll() error

AddAll stages the entire worktree, honouring .gitignore rules so ignored paths (build artefacts and the like) are not committed. The .gitignore file itself is staged. It wraps go-git's Worktree.AddWithOptions{All: true}, which applies the repository's ignore patterns, which a plain Add(".") would not.

Returns ErrNoWorktree if the worktree has not been initialised.

func (*Repo) AddToFS

func (r *Repo) AddToFS(fs afero.Fs, gitFile *object.File, fullPath string) error

AddToFS ensures that a git file is available in the afero filesystem.

func (*Repo) Checkout

func (r *Repo) Checkout(branch plumbing.ReferenceName) error

Checkout checks out the given branch. Returns ErrNoWorktree if the worktree has not been initialised.

func (*Repo) CheckoutCommit

func (r *Repo) CheckoutCommit(hash plumbing.Hash) error

CheckoutCommit checks out a specific commit by hash. Returns ErrNoWorktree if the worktree has not been initialised.

func (*Repo) Clone

func (r *Repo) Clone(ctx context.Context, uri string, targetPath string, opts ...CloneOption) (*git.Repository, *git.Worktree, error)

Clone clones a git repository to a target path on the filesystem Supports both remote URLs and local git repository paths with clone options.

func (*Repo) Commit

func (r *Repo) Commit(_ context.Context, commitMsg string, opts *git.CommitOptions) (plumbing.Hash, error)

Commit records a commit on the current worktree. Returns ErrNoWorktree if the worktree has not been initialised. The context is accepted for Committer signature symmetry; committing to the local worktree performs no network I/O.

func (*Repo) CreateBranch

func (r *Repo) CreateBranch(ctx context.Context, branchName string) error

CreateBranch creates a branch in the git tree. Returns ErrNoRepository / ErrNoWorktree if the repo has not been opened.

func (*Repo) CreateRemote

func (r *Repo) CreateRemote(name string, urls []string) (*git.Remote, error)

CreateRemote creates a named remote with the given URLs. Returns ErrNoRepository if the repository has not been initialised.

func (*Repo) CreateTag added in v0.4.0

func (r *Repo) CreateTag(_ context.Context, req TagRequest) (plumbing.Hash, error)

CreateTag creates an annotated tag at req.Commit.

It does not push. Publishing is Committer.Push with the refspec the caller wants, which keeps both steps visible and leaves the caller to decide what a failed push should do about the local tag.

Returns an error wrapping ErrTagAlreadyExists when the tag exists. This never moves one: a published tag has been fetched by consumers, and moving it means two artefacts answer to one version.

func (*Repo) DirectoryExists

func (r *Repo) DirectoryExists(relPath string) (bool, error)

DirectoryExists checks if a directory exists in the git repository at the given relative path In git, directories don't exist as separate entities - we check if any files exist under the path. Returns ErrNoRepository if the repository has not been initialised.

func (*Repo) FileExists

func (r *Repo) FileExists(relPath string) (bool, error)

FileExists checks if a file exists in the git repository at the given relative path. Returns ErrNoRepository if the repository has not been initialised.

func (*Repo) GetAuth

func (r *Repo) GetAuth() transport.AuthMethod

func (*Repo) GetFile

func (r *Repo) GetFile(relPath string) (*object.File, error)

GetFile retrieves a file from the git repository at the given relative path. Returns ErrNoRepository if the repository has not been initialised.

func (*Repo) InitLocal

func (r *Repo) InitLocal(location, branch string) (*git.Repository, *git.Worktree, error)

InitLocal initialises a new git repository at location on the named default branch and binds it (and its worktree) to the receiver. It is the init-only counterpart to OpenLocal: if location is already inside a repository it returns ErrAlreadyRepository rather than silently opening the existing one, so callers can keep init and open as separate, explicit decisions.

branch defaults to "main" when empty.

func (*Repo) Open

func (r *Repo) Open(ctx context.Context, repoType RepoType, location string, branch string, opts ...CloneOption) (*git.Repository, *git.Worktree, error)

Open opens a repository of the given type. For a local repository it opens (or initialises) location on disk; for an in-memory repository it clones location into memory. The branch argument applies only to the in-memory path (the local path defaults to "main" when initialising).

func (*Repo) OpenInMemory

func (r *Repo) OpenInMemory(ctx context.Context, location string, branch string, opts ...CloneOption) (*git.Repository, *git.Worktree, error)

func (*Repo) OpenLocal

func (r *Repo) OpenLocal(_ context.Context, location string) (*git.Repository, *git.Worktree, error)

OpenLocal opens the local git repository at location, initialising a fresh repository (on default branch "main") only when none exists there. It deliberately distinguishes "no repository" from "corrupt repository": the init fallback is gated on git.ErrRepositoryNotExists, so any other open failure (a malformed .git, a path that is not a directory, and the like) is returned wrapped rather than masked by an init attempt that would clobber the on-disk state.

A **linked** git worktree (one created by "git worktree add", whose .git is a file redirecting to <parent>/.git/worktrees/<name>/) is read correctly: HEAD and the index come from the worktree's own directory, while refs and objects come from the common directory named by the commondir file beside them. That is what EnableDotGitCommonDir buys, and it is not optional. Without it this returns a nil error and a handle that fails on first use, which is worse than failing to open. An ordinary checkout has no commondir file and takes exactly the same path it always did.

When the git directory a .git file names has gone (the parent pruned, moved or re-cloned) this returns ErrMissingGitDir and writes nothing. The discriminator is that .git is present and is not a directory, so a --separate-git-dir checkout and a submodule reach it identically; all three use the same redirect file. A .git *directory* that go-git cannot open is a different case and still takes the init fallback, matching "git init".

The context is accepted for Opener signature symmetry; opening or initialising a local repository performs no network I/O.

func (*Repo) Push

func (r *Repo) Push(ctx context.Context, opts *git.PushOptions) error

Push pushes the repository using the configured auth when opts.Auth is nil. Returns ErrNoRepository if the repository has not been initialised.

func (*Repo) Remote

func (r *Repo) Remote(name string) (*git.Remote, error)

Remote returns the named remote. Returns ErrNoRepository if the repository has not been initialised.

func (*Repo) SetBasicAuth

func (r *Repo) SetBasicAuth(username, password string)

func (*Repo) SetKey

func (r *Repo) SetKey(key *ssh.PublicKeys)

func (*Repo) SetRepo

func (r *Repo) SetRepo(repo *git.Repository)

func (*Repo) SetSource

func (r *Repo) SetSource(source int)

func (*Repo) SetTree

func (r *Repo) SetTree(tree *git.Worktree)

func (*Repo) SourceIs

func (r *Repo) SourceIs(source int) bool

func (*Repo) WalkTree

func (r *Repo) WalkTree(fn func(*object.File) error) error

WalkTree walks the git tree and calls the provided function for each file. Returns ErrNoRepository if the repository has not been initialised.

func (*Repo) WithRepo

func (r *Repo) WithRepo(fn func(*git.Repository) error) error

WithRepo calls fn with the underlying *git.Repository. Repo is not safe for concurrent use; callers are responsible for external synchronisation if sharing a *Repo across goroutines.

Returns ErrNoRepository if the repository has not been initialised.

func (*Repo) WithTree

func (r *Repo) WithTree(fn func(*git.Worktree) error) error

WithTree calls fn with the underlying *git.Worktree. Repo is not safe for concurrent use; callers are responsible for external synchronisation if sharing a *Repo across goroutines.

Returns ErrNoWorktree if the worktree has not been initialised.

func (*Repo) WithWorkFS

func (r *Repo) WithWorkFS(fn func(afero.Fs) error) error

WithWorkFS runs fn with an afero view of the worktree.

func (*Repo) WorkFS

func (r *Repo) WorkFS() (afero.Fs, error)

WorkFS returns the worktree as an afero.Fs. *Repo is not safe for concurrent use, so the view uses the default no-op locker; the caller owns any external synchronisation.

type RepoLike

RepoLike is the full git-repository interface, composed of focused role interfaces. Prefer depending on the narrower roles (TreeReader, Committer, …) where possible. Its method set is identical to the sum of the embedded roles, so *Repo and *ThreadSafeRepo satisfy it unchanged.

type RepoOpt

type RepoOpt func(*Repo) error

RepoOpt is a functional option for configuring a Repo.

func WithConfig

func WithConfig(cfg *config.Config) RepoOpt

WithConfig sets the go-git config for the repository.

type RepoType

type RepoType string

RepoType identifies the repository backend (local filesystem or in-memory). It is a defined type rather than a bare string alias so the backend discriminator carries its own type at call sites.

const (
	LocalRepo    RepoType = "local"
	InMemoryRepo RepoType = "inmemory"
)

type SSHSettings

type SSHSettings struct {
	Configured bool
	HasKey     bool
	Type       string
	Path       string
}

SSHSettings describes the forge SSH key configuration resolved by the caller. Configured tracks whether the forge's SSH block exists at all; HasKey distinguishes a present-but-scalar SSH block from a structured ssh.key block.

Path is a resolved filesystem path, never an environment-variable name: this package reads no environment of its own, so that every input arrives through Settings. Use KeyPath to apply the usual "explicit path, else named environment variable" precedence at the call site.

type Settings

type Settings struct {
	// Forge selects the git-over-HTTPS authentication convention: one of the
	// Forge* constants, or any other string (treated as GitHub). Empty defaults
	// to GitHub.
	Forge string

	// Private marks the target repository as private, which makes a missing
	// token a fast failure rather than an unauthenticated attempt.
	Private bool

	// AuthEnabled requests token authentication even when no token has been
	// supplied, so a missing credential is reported rather than silently skipped.
	AuthEnabled bool

	// Token supplies the access token, resolved lazily. See [TokenSource].
	Token TokenSource

	SSH    SSHSettings
	Logger Logger

	// FS is the filesystem used to read SSH key files. Defaults to
	// afero.NewOsFs() when nil.
	FS afero.Fs

	// Progress receives the remote server's sideband output during clone, pull
	// and push: the stream real git renders as "Receiving objects: 45%" and
	// "remote: ..." lines. Nil discards it.
	//
	// This is the server's channel, not this package's: it carries pre-receive
	// hook rejection reasons and the "create a merge request at ..." URLs that
	// arrive nowhere else, so discarding it loses information Logger cannot
	// reproduce. Being remote-controlled text, it belongs on a terminal or in a
	// buffer rather than piped unsanitised into a log aggregator.
	//
	// It is a Settings field rather than an environment-variable switch so the
	// decision is made by the caller at construction, is scoped to one
	// repository rather than the whole process, and can be captured in a test
	// without mutating global state.
	Progress io.Writer
}

Settings contains the typed configuration NewRepo needs to resolve git authentication without depending on a forge abstraction, a config container, or a DI container. Callers resolve those concerns and pass the results here.

type SourceState

type SourceState interface {
	SourceIs(int) bool
	SetSource(int)
}

SourceState discriminates the repository backend (in-memory vs local).

type TagRequest added in v0.4.0

type TagRequest struct {
	// Name is the tag, without the refs/tags/ prefix — "v1.2.3".
	Name string

	// Commit is the FULL 40-character SHA the tag points at. It is validated,
	// never resolved.
	//
	// A full SHA, and a string rather than a plumbing.Hash, because
	// plumbing.NewHash does not report a bad value and its failure mode for
	// the likeliest mistake is the dangerous one: an ABBREVIATED sha is
	// silently zero-padded into a different, valid-looking hash that is not
	// zero. A guard written as hash.IsZero() lets it through, and the tag then
	// names an object that does not exist.
	//
	// Resolving a branch name here is refused rather than supported. A caller
	// reaching this point has already established which commit to tag — in a
	// release workflow, by asking the forge what actually landed — and
	// re-resolving a name would reopen the race that check exists to close.
	Commit string

	// Message annotates the tag. Required: this creates annotated tags only,
	// because a release tag should record who made it and when rather than
	// being a bare pointer.
	Message string

	// Tagger identifies who made the tag. REQUIRED.
	//
	// go-git reads the name and email from the machine's git configuration
	// when this is nil, which attributes the same release to different people
	// depending on where it was cut — and in CI, to whatever identity the
	// runner image carries.
	Tagger *object.Signature
}

TagRequest describes one annotated tag. See spec 0006.

type Tagger added in v0.4.0

type Tagger interface {
	CreateTag(context.Context, TagRequest) (plumbing.Hash, error)
}

Tagger creates annotated tags.

Its own role rather than a method on Committer because they are not the same job: a caller that tags a release does not commit, and one that writes a branch does not tag. Widening Committer would give every existing consumer a dependency on an operation it does not perform, without changing the interface it names. See spec 0006 D1.

CreateTag accepts a context.Context for signature symmetry with the rest of the module; it performs no network I/O, because tagging is local and publishing is Committer.Push.

type ThreadSafeRepo

type ThreadSafeRepo struct {
	// contains filtered or unexported fields
}

ThreadSafeRepo wraps a *Repo with a mutex so that all RepoLike methods are safe to call concurrently from multiple goroutines.

Thread-safety guarantee

Every method acquires the internal mutex for its full duration. Concurrent callers are serialised; no two calls to any method execute simultaneously.

WithRepo and WithTree

These are the only way to interact with the underlying go-git objects safely. The callback executes while the mutex is held. Callers must not:

  • Retain the pointer after the callback returns.
  • Call any ThreadSafeRepo method from inside the callback (deadlock).
  • Spawn goroutines inside the callback that access the pointer after it returns.

go-git concurrency model

go-git mutates internal caches during read operations. ThreadSafeRepo uses sync.Mutex (exclusive) rather than sync.RWMutex; concurrent reads are not permitted.

func NewThreadSafeRepo

func NewThreadSafeRepo(settings Settings, opts ...RepoOpt) (*ThreadSafeRepo, error)

NewThreadSafeRepo creates a ThreadSafeRepo backed by a freshly constructed *Repo. The settings and opts arguments have the same semantics as NewRepo.

func (*ThreadSafeRepo) AddAll

func (r *ThreadSafeRepo) AddAll() error

func (*ThreadSafeRepo) AddToFS

func (r *ThreadSafeRepo) AddToFS(fs afero.Fs, gitFile *object.File, fullPath string) error

func (*ThreadSafeRepo) Checkout

func (r *ThreadSafeRepo) Checkout(branch plumbing.ReferenceName) error

func (*ThreadSafeRepo) CheckoutCommit

func (r *ThreadSafeRepo) CheckoutCommit(hash plumbing.Hash) error

func (*ThreadSafeRepo) Clone

func (r *ThreadSafeRepo) Clone(ctx context.Context, uri string, targetPath string, opts ...CloneOption) (*git.Repository, *git.Worktree, error)

func (*ThreadSafeRepo) Commit

func (r *ThreadSafeRepo) Commit(ctx context.Context, commitMsg string, opts *git.CommitOptions) (plumbing.Hash, error)

func (*ThreadSafeRepo) CreateBranch

func (r *ThreadSafeRepo) CreateBranch(ctx context.Context, branchName string) error

func (*ThreadSafeRepo) CreateTag added in v0.4.0

func (r *ThreadSafeRepo) CreateTag(ctx context.Context, req TagRequest) (plumbing.Hash, error)

func (*ThreadSafeRepo) DirectoryExists

func (r *ThreadSafeRepo) DirectoryExists(relPath string) (bool, error)

func (*ThreadSafeRepo) FileExists

func (r *ThreadSafeRepo) FileExists(relPath string) (bool, error)

func (*ThreadSafeRepo) GetAuth

func (r *ThreadSafeRepo) GetAuth() transport.AuthMethod

func (*ThreadSafeRepo) GetFile

func (r *ThreadSafeRepo) GetFile(relPath string) (*object.File, error)

func (*ThreadSafeRepo) InitLocal

func (r *ThreadSafeRepo) InitLocal(location string, branch string) (*git.Repository, *git.Worktree, error)

func (*ThreadSafeRepo) Open

func (r *ThreadSafeRepo) Open(ctx context.Context, repoType RepoType, location string, branch string, opts ...CloneOption) (*git.Repository, *git.Worktree, error)

func (*ThreadSafeRepo) OpenInMemory

func (r *ThreadSafeRepo) OpenInMemory(ctx context.Context, location string, branch string, opts ...CloneOption) (*git.Repository, *git.Worktree, error)

func (*ThreadSafeRepo) OpenLocal

func (r *ThreadSafeRepo) OpenLocal(ctx context.Context, location string) (*git.Repository, *git.Worktree, error)

func (*ThreadSafeRepo) Push

func (r *ThreadSafeRepo) Push(ctx context.Context, opts *git.PushOptions) error

func (*ThreadSafeRepo) SetBasicAuth

func (r *ThreadSafeRepo) SetBasicAuth(username, password string)

func (*ThreadSafeRepo) SetKey

func (r *ThreadSafeRepo) SetKey(key *ssh.PublicKeys)

func (*ThreadSafeRepo) SetRepo

func (r *ThreadSafeRepo) SetRepo(repo *git.Repository)

func (*ThreadSafeRepo) SetSource

func (r *ThreadSafeRepo) SetSource(source int)

func (*ThreadSafeRepo) SetTree

func (r *ThreadSafeRepo) SetTree(tree *git.Worktree)

func (*ThreadSafeRepo) SourceIs

func (r *ThreadSafeRepo) SourceIs(source int) bool

func (*ThreadSafeRepo) WalkTree

func (r *ThreadSafeRepo) WalkTree(fn func(*object.File) error) error

func (*ThreadSafeRepo) WithRepo

func (r *ThreadSafeRepo) WithRepo(fn func(*git.Repository) error) error

WithRepo acquires the mutex and calls fn with the underlying *git.Repository. The callback executes while the lock is held.

Returns ErrNoRepository if the repository has not been initialised.

func (*ThreadSafeRepo) WithTree

func (r *ThreadSafeRepo) WithTree(fn func(*git.Worktree) error) error

WithTree acquires the mutex and calls fn with the underlying *git.Worktree. The callback executes while the lock is held.

Returns ErrNoWorktree if the worktree has not been initialised.

func (*ThreadSafeRepo) WithWorkFS

func (r *ThreadSafeRepo) WithWorkFS(fn func(afero.Fs) error) error

WithWorkFS runs fn with an afero view of the worktree while holding the repo lock for the whole callback (via WithTree). The view uses a no-op locker because the lock is already held, and re-locking the non-reentrant mutex would deadlock.

func (*ThreadSafeRepo) WorkFS

func (r *ThreadSafeRepo) WorkFS() (afero.Fs, error)

WorkFS returns the worktree as an afero.Fs whose every operation re-locks the repo mutex, so the returned handle is safe for concurrent use even though the worktree filesystem reference escapes this method (see WorktreeFS.WorkFS).

type TokenSource

type TokenSource func() string

TokenSource returns an access token for git-over-HTTPS authentication.

It is a function rather than a resolved string because resolution can be expensive or interactive: a token may live in the OS keychain, where an eager lookup could prompt the user to unlock it. The repository calls the source only on the code path that actually authenticates with a token: SSH authentication takes priority, and a repository configured for SSH never invokes it at all.

Use StaticToken when the token is already in hand.

func StaticToken

func StaticToken(token string) TokenSource

StaticToken adapts an already-resolved token to a TokenSource. An empty token is treated as "no token available".

type TreeReader

type TreeReader interface {
	WalkTree(func(*object.File) error) error
	FileExists(string) (bool, error)
	DirectoryExists(string) (bool, error)
	GetFile(string) (*object.File, error)
	AddToFS(fs afero.Fs, gitFile *object.File, fullPath string) error
}

TreeReader provides read-only queries over the committed git tree.

type WorktreeController

type WorktreeController interface {
	SetTree(*git.Worktree)
	Checkout(plumbing.ReferenceName) error
	CheckoutCommit(plumbing.Hash) error
}

WorktreeController manages working-tree and checkout state.

type WorktreeFS

type WorktreeFS interface {
	// WorkFS returns the active worktree's filesystem as an afero.Fs, or
	// [ErrNoWorktree] if no worktree is open.
	//
	// On [ThreadSafeRepo] the returned FS is safe for concurrent use: every
	// operation re-locks the repo mutex. Operations are individually atomic, but
	// a SEQUENCE is not, because a concurrent Commit/Add may interleave between two
	// writes. Use [WorktreeFS.WithWorkFS] when a sequence must be atomic.
	//
	// The returned FS (and any file it opens) MUST NOT be used from inside a
	// WithWorkFS / WithTree / WithRepo callback: that region already holds the
	// repo's (non-reentrant) mutex, so re-locking would deadlock.
	WorkFS() (afero.Fs, error)

	// WithWorkFS runs fn with an afero view of the worktree while holding the
	// repo lock for the whole callback, so the sequence is atomic relative to
	// other repo operations. The afero.Fs passed to fn is unsynchronised (the
	// lock is already held); fn MUST NOT retain it past return, call any other
	// repo method, or open a WorkFS() handle.
	WithWorkFS(fn func(afero.Fs) error) error
}

WorktreeFS is a RepoLike role exposing the active worktree as an afero.Fs, so afero-based code can read and write an in-memory (or local) git worktree directly: single source of truth, no materialise/sync step (contrast with TreeReader.AddToFS, which copies a git tree into a *separate* afero FS).

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL