Files

116 lines
3.3 KiB
Go

// Package forge is what hotdog-cms knows about the places sites are hosted in
// git: GitHub (and GitHub Enterprise), GitLab, Gitea, Forgejo (Codeberg),
// Gogs, Bitbucket Cloud, and plain git anywhere else.
//
// For each it can verify and read push webhooks, so a push rebuilds at once
// instead of at the next check, and post a commit status, so a pull request
// shows its preview's address. Plain git gets a token-checked hook any
// post-receive script can call; it has no statuses to post.
package forge
import (
"fmt"
"net/url"
"regexp"
"strings"
)
// Kind names a platform.
type Kind string
const (
GitHub Kind = "github"
GitLab Kind = "gitlab"
Gitea Kind = "gitea"
Forgejo Kind = "forgejo"
Gogs Kind = "gogs"
Bitbucket Kind = "bitbucket"
Git Kind = "git"
)
// Kinds lists every platform, for flags and help text.
var Kinds = []Kind{GitHub, GitLab, Gitea, Forgejo, Gogs, Bitbucket, Git}
// Repo is a repository on a platform.
type Repo struct {
Kind Kind
Host string // host[:port] of the web/API server
Path string // owner/name, or group/subgroup/name on GitLab
API string // API base URL
}
// TokenUser is the user name each platform expects with a token over HTTPS.
func (k Kind) TokenUser() string {
switch k {
case GitHub:
return "x-access-token"
case GitLab:
return "oauth2"
case Bitbucket:
return "x-token-auth"
}
// Gitea takes the token as the password with any user name (checked on
// Gitea 28.1). Forgejo is a fork of it; for Gogs, set HOTDOG_GIT_USER to the
// account's name if the default is refused.
return "hotdog-cms"
}
var scpRe = regexp.MustCompile(`^(?:[A-Za-z0-9._-]+@)?([A-Za-z0-9.-]+):(.+)$`)
// Parse works out the platform and repository from a clone URL. kind may be
// empty to guess from well-known hosts; a self-hosted Gitea, GitLab or GitHub
// Enterprise needs it said.
func Parse(cloneURL string, kind Kind) (Repo, error) {
var host, p string
if u, err := url.Parse(cloneURL); err == nil && u.Scheme != "" && u.Host != "" {
host, p = u.Host, u.Path
if u.Scheme == "ssh" {
host = u.Hostname() // an SSH port is not the web server's port
}
} else if m := scpRe.FindStringSubmatch(cloneURL); m != nil && !strings.HasPrefix(cloneURL, "/") {
host, p = m[1], m[2]
} else {
// A local path or file:// URL: plain git, nothing to talk to.
return Repo{Kind: Git}, nil
}
p = strings.TrimSuffix(strings.Trim(p, "/"), ".git")
if kind == "" {
switch strings.ToLower(host) {
case "github.com":
kind = GitHub
case "gitlab.com":
kind = GitLab
case "codeberg.org":
kind = Forgejo
case "gitea.com":
kind = Gitea
case "bitbucket.org":
kind = Bitbucket
default:
kind = Git
}
}
r := Repo{Kind: kind, Host: host, Path: p}
switch kind {
case GitHub:
if strings.EqualFold(host, "github.com") {
r.API = "https://api.github.com"
} else {
r.API = "https://" + host + "/api/v3"
}
case GitLab:
r.API = "https://" + host + "/api/v4"
case Gitea, Forgejo, Gogs:
r.API = "https://" + host + "/api/v1"
case Bitbucket:
r.API = "https://api.bitbucket.org/2.0"
case Git:
default:
return Repo{}, fmt.Errorf("unknown platform %q (one of %v)", kind, Kinds)
}
if kind != Git && strings.Count(p, "/") < 1 {
return Repo{}, fmt.Errorf("%s doesn't look like owner/repository", cloneURL)
}
return r, nil
}