Repo¶
Package: pkg/vcs/repo
Provides git repository operations backed by go-git. Supports both local filesystem storage and in-memory storage (memfs), and integrates with afero for testable file operations.
Constructor¶
NewRepo reads authentication from props.Config and returns a configured *Repo. Authentication is resolved automatically (see Authentication) โ you rarely need to call SetKey or SetBasicAuth directly.
Options:
// Inject a custom go-git config (advanced โ rarely needed)
func WithConfig(cfg *config.Config) RepoOpt
Storage Strategies¶
In-memory (SourceMemory)¶
The repository lives entirely in RAM using go-git's memfs/memory.Storage. No files are written to disk.
gitRepo, worktree, err := r.OpenInMemory(url, "main",
repo.WithShallowClone(1),
repo.WithSingleBranch("main"),
repo.WithNoTags(),
)
Use for: temporary analysis, code generation, CI pipelines where disk artifacts are undesirable.
Local filesystem (SourceLocal)¶
Opens an existing repository with git.PlainOpen, or initialises a new one if the directory does not contain a git repo.
Use for: persistent working trees, development tools that need a full working directory.
Clone to disk¶
Clone is distinct from OpenLocal โ it clones a remote URL to a target path.
gitRepo, worktree, err := r.Clone(url, "/path/to/target",
repo.WithShallowClone(1),
repo.WithNoTags(),
)
Polymorphic open¶
Open dispatches to OpenLocal or OpenInMemory based on the RepoType string:
Clone Options¶
| Function | Effect |
|---|---|
WithShallowClone(depth int) |
Fetch only the last depth commits |
WithSingleBranch(branch string) |
Limit fetch to the named branch |
WithNoTags() |
Skip tag fetch |
WithRecurseSubmodules() |
Initialise submodules after clone |
Branch Operations¶
// Create a branch (checks out an existing branch and pulls if it already exists)
err := r.CreateBranch("feature/my-feature")
// Checkout an existing branch
err := r.Checkout(plumbing.NewBranchReferenceName("main"))
// Detached HEAD at a specific commit
err := r.CheckoutCommit(hash)
Commit and Push¶
// Create a commit on the current worktree
hash, err := r.Commit("chore: update generated files", &git.CommitOptions{
Author: &object.Signature{
Name: "My Tool",
Email: "[email protected]",
When: time.Now(),
},
})
// Push with the pre-configured auth
err := r.Push(nil) // nil uses default PushOptions with repo auth
Tree Operations¶
All tree operations work against the HEAD commit of the current repository state.
Walk all files¶
err := r.WalkTree(func(f *object.File) error {
content, err := f.Contents()
// process f.Name, content ...
return err
})
Check existence¶
DirectoryExists returns true if any file under that path prefix exists โ git has no directory objects.
Retrieve a single file¶
Bridge to afero¶
AddToFS copies a go-git file object into an afero.Fs. The target file is skipped if it already exists.
This is the standard pattern for hydrating a virtual filesystem with files from any point in a repository's history.
Authentication¶
NewRepo configures authentication from props.Config automatically:
| Priority | Condition | Auth method |
|---|---|---|
| 1 | github.ssh.key.type = "agent" |
SSH agent (ssh.DefaultAuthBuilder) |
| 2 | github.ssh.key.path or $GITHUB_KEY set |
Identity file (GetSSHKey) |
| 3 | No github.ssh config at all |
PAT via GITHUB_TOKEN (basic auth x-access-token:<token>) |
You can override auth manually after construction:
r.SetKey(publicKeys) // SSH
r.SetBasicAuth("user", "pass") // Basic / PAT
auth := r.GetAuth() // Retrieve current method
GetSSHKey¶
Reads a PEM private key from localfs. If the key is passphrase-protected, prompts the user interactively via a charmbracelet/huh input form.
Source Constants¶
const (
SourceUnknown = iota // 0 โ not yet configured
SourceMemory // 1 โ in-memory storage
SourceLocal // 2 โ filesystem storage
)
var (
LocalRepo RepoType = "local"
InMemoryRepo RepoType = "inmemory"
)
r.SourceIs(repo.SourceMemory) and r.SetSource(repo.SourceLocal) are available for code that needs to branch on storage type.
RepoLike Interface¶
All methods above are part of RepoLike. Functions that accept a repository should depend on RepoLike rather than *Repo:
type RepoLike interface {
SourceIs(int) bool
SetSource(int)
SetRepo(*git.Repository)
SetKey(*ssh.PublicKeys)
SetBasicAuth(string, string)
GetAuth() transport.AuthMethod
SetTree(*git.Worktree)
Checkout(plumbing.ReferenceName) error
CheckoutCommit(plumbing.Hash) error
CreateBranch(string) error
Push(*git.PushOptions) error
Commit(string, *git.CommitOptions) (plumbing.Hash, error)
OpenInMemory(string, string, ...CloneOption) (*git.Repository, *git.Worktree, error)
OpenLocal(string, string) (*git.Repository, *git.Worktree, error)
Open(RepoType, string, string, ...CloneOption) (*git.Repository, *git.Worktree, error)
Clone(string, string, ...CloneOption) (*git.Repository, *git.Worktree, error)
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
WithRepo(func(*git.Repository) error) error
WithTree(func(*git.Worktree) error) error
}
Thread Safety¶
go-git is not thread-safe โ its internal caches mutate during reads. Two types are available depending on your concurrency needs:
Repo (default, no locking)¶
Use *Repo when the repository is accessed from a single goroutine. WithRepo and WithTree simply call the callback with the underlying pointer:
Returns ErrNoRepository / ErrNoWorktree if the repository or worktree has not been initialised.
ThreadSafeRepo (opt-in mutex wrapper)¶
Use *ThreadSafeRepo when sharing a repository across goroutines. Every method acquires an exclusive mutex for its full duration.
ts, err := repo.NewThreadSafeRepo(props)
// Safe to call from multiple goroutines:
err = ts.WithRepo(func(gr *git.Repository) error {
// mutex is held for the duration of this callback
return nil
})
Caveats:
- Do not retain the pointer received in
WithRepo/WithTreeafter the callback returns. - Do not call any
ThreadSafeRepomethod from inside a callback โsync.Mutexis not re-entrant and this will deadlock. Open*/Clonereturn raw pointers for setup-time convenience. Do not share these across goroutines; useWithRepo/WithTreefor subsequent concurrent access.
Testing¶
Use the generated mock for unit tests:
import mock_repo "gitlab.com/phpboyscout/go-tool-base/mocks/pkg/vcs/repo"
mockRepo := mock_repo.NewMockRepoLike(t)
mockRepo.EXPECT().CreateBranch("feature/test").Return(nil)
mockRepo.EXPECT().Checkout(plumbing.NewBranchReferenceName("feature/test")).Return(nil)
For integration-style tests without network access, use git.PlainInit in a t.TempDir():
tmpDir := t.TempDir()
gitRepo, err := git.PlainInit(tmpDir, false)
// Add a commit, then call r.OpenLocal(tmpDir, "main")
Enable git progress output in tests by setting GTB_GIT_ENABLE_PROGRESS=1.
Related Documentation¶
- VCS index โ package overview and authentication helper
- GitHub โ GitHub API client (separate from git operations)
- VCS Concepts โ bridged filesystem pattern and storage strategy rationale