Files
certman/common/git.go

128 lines
3.7 KiB
Go

package common
import (
"errors"
"fmt"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5"
gitconf "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/storage/memory"
)
// GitWorkspace is an in-memory git working tree for a single domain's
// certificate repository. It is the shared primitive that both client and
// server modes operate on: client mode clones and reads, server mode
// init/clones and pushes. The struct carries no mode-specific state.
type GitWorkspace struct {
Domain string
URL string
Storage *memory.Storage
FS billy.Filesystem
Repo *git.Repository
WorkTree *git.Worktree
}
// GitSource identifies a supported git repository host.
type GitSource int
const (
Github GitSource = iota
Gitlab
Gitea
Gogs
Bitbucket
CodeCommit
)
// GitSourceName maps GitSource to the string used in the app config's
// git.host field.
var GitSourceName = map[GitSource]string{
Github: "github",
Gitlab: "gitlab",
Gitea: "gitea",
Gogs: "gogs",
Bitbucket: "bitbucket",
CodeCommit: "code-commit",
}
// StrToGitSource parses a config string (e.g. "gitea") into a GitSource.
func StrToGitSource(s string) (GitSource, error) {
for k, v := range GitSourceName {
if v == s {
return k, nil
}
}
return 0, fmt.Errorf("invalid git source %q", s)
}
// ErrRepoNotFound is returned by RepoProvider implementations when a domain's
// repository does not exist on the remote. Callers use it to distinguish
// "repo hasn't been created yet" from transport or auth failures.
var ErrRepoNotFound = errors.New("repository not found")
// NewGitWorkspace returns a workspace with an in-memory filesystem and storage
// wired up. Call InitRepo (for a brand-new remote) or CloneRepo (for an
// existing one) to populate Repo and WorkTree.
func NewGitWorkspace(domain, url string) *GitWorkspace {
return &GitWorkspace{
Domain: domain,
URL: url,
Storage: memory.NewStorage(),
FS: memfs.New(),
}
}
// RepoURL builds the canonical clone URL for a domain's certificate repo. It
// is the single authoritative place for the "<server>/<org>/<domain><suffix>.git"
// pattern so callers do not assemble URLs by hand.
func RepoURL(config *AppConfig, domainConfig *DomainConfig, domain string) string {
return config.Git.Server + "/" + config.Git.OrgName + "/" + domain + domainConfig.Repo.RepoSuffix + ".git"
}
// InitRepo initializes an empty local repository in ws and registers origin
// pointed at ws.URL. Use this on the first push for a new domain; use
// CloneRepo on subsequent runs.
func InitRepo(ws *GitWorkspace) error {
repo, err := git.Init(ws.Storage, ws.FS)
if err != nil {
return fmt.Errorf("git init: %w", err)
}
if _, err := repo.CreateRemote(&gitconf.RemoteConfig{
Name: "origin",
URLs: []string{ws.URL},
}); err != nil && !errors.Is(err, git.ErrRemoteExists) {
return fmt.Errorf("add remote: %w", err)
}
wt, err := repo.Worktree()
if err != nil {
return fmt.Errorf("get worktree: %w", err)
}
ws.Repo = repo
ws.WorkTree = wt
return nil
}
// CloneRepo clones ws.URL into ws using the git credentials from config. It
// performs no ownership or mode-specific checks: server mode must follow up
// with server.VerifyOwnership before pushing.
func CloneRepo(ws *GitWorkspace, config *AppConfig) error {
auth := &http.BasicAuth{
Username: config.Git.Username,
Password: config.Git.APIToken,
}
repo, err := git.Clone(ws.Storage, ws.FS, &git.CloneOptions{URL: ws.URL, Auth: auth})
if err != nil {
return fmt.Errorf("git clone %s: %w", ws.URL, err)
}
wt, err := repo.Worktree()
if err != nil {
return fmt.Errorf("get worktree: %w", err)
}
ws.Repo = repo
ws.WorkTree = wt
return nil
}