Files
gitea-tea/modules/context/context.go
Lunny Xiao 7530fb23a6 Fix lint
2025-10-03 12:25:46 -07:00

212 lines
6.5 KiB
Go

// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package context
import (
"errors"
"fmt"
"os"
"path"
"strconv"
"strings"
"time"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/debug"
"code.gitea.io/tea/modules/git"
"github.com/urfave/cli/v3"
)
// ErrNotAGiteaRepo is returned when no Gitea login could be found for a local git repo
var ErrNotAGiteaRepo = errors.New("no Gitea login found. You might want to specify --repo (and --login) to work outside of a repository")
// TeaContext contains all context derived during command initialization and wraps cli.Context
type TeaContext struct {
*cli.Command
Login *config.Login // config data & client for selected login
RepoSlug string // <owner>/<repo>, optional
Owner string // repo owner as derived from context or provided in flag, optional
Repo string // repo name as derived from context or provided in flag, optional
Output string // value of output flag
LocalRepo *git.TeaRepo // is set if flags specified a local repo via --repo, or if $PWD is a git repo
}
// GetRemoteRepoHTMLURL returns the web-ui url of the remote repo,
// after ensuring a remote repo is present in the context.
func (ctx *TeaContext) GetRemoteRepoHTMLURL() string {
ctx.Ensure(CtxRequirement{RemoteRepo: true})
return path.Join(ctx.Login.URL, ctx.Owner, ctx.Repo)
}
// Ensure checks if requirements on the context are set, and terminates otherwise.
func (ctx *TeaContext) Ensure(req CtxRequirement) {
if req.LocalRepo && ctx.LocalRepo == nil {
fmt.Println("Local repository required: Execute from a repo dir, or specify a path with --repo.")
os.Exit(1)
}
if req.RemoteRepo && len(ctx.RepoSlug) == 0 {
fmt.Println("Remote repository required: Specify ID via --repo or execute from a local git repo.")
os.Exit(1)
}
}
// CtxRequirement specifies context needed for operation
type CtxRequirement struct {
// ensures a local git repo is available & ctx.LocalRepo is set. Implies .RemoteRepo
LocalRepo bool
// ensures ctx.RepoSlug, .Owner, .Repo are set
RemoteRepo bool
}
// FromLocalRepo discovers login & repo slug from the default branch remote of the given local repo
func FromLocalRepo(repoPath, remoteValue string) (*git.TeaRepo, *config.Login, string, error) {
repo, err := git.RepoFromPath(repoPath)
if err != nil {
return nil, nil, "", err
}
gitConfig, err := repo.Config()
if err != nil {
return repo, nil, "", err
}
debug.Printf("Get git config %v of %s in repo %s", gitConfig, remoteValue, repoPath)
if len(gitConfig.Remotes) == 0 {
return repo, nil, "", ErrNotAGiteaRepo
}
// When no preferred value is given, choose a remote to find a
// matching login based on its URL.
if len(gitConfig.Remotes) > 1 && len(remoteValue) == 0 {
// if master branch is present, use it as the default remote
mainBranches := []string{"main", "master", "trunk"}
for _, b := range mainBranches {
masterBranch, ok := gitConfig.Branches[b]
if ok {
if len(masterBranch.Remote) > 0 {
remoteValue = masterBranch.Remote
}
break
}
}
// if no branch has matched, default to origin or upstream remote.
if len(remoteValue) == 0 {
if _, ok := gitConfig.Remotes["upstream"]; ok {
remoteValue = "upstream"
} else if _, ok := gitConfig.Remotes["origin"]; ok {
remoteValue = "origin"
}
}
}
// make sure a remote is selected
if len(remoteValue) == 0 {
for remote := range gitConfig.Remotes {
remoteValue = remote
break
}
}
remoteConfig, ok := gitConfig.Remotes[remoteValue]
if !ok || remoteConfig == nil {
return repo, nil, "", fmt.Errorf("remote '%s' not found in this Git repository", remoteValue)
}
debug.Printf("Get remote configurations %v of %s in repo %s", remoteConfig, remoteValue, repoPath)
logins, err := config.GetLogins()
if err != nil {
return repo, nil, "", err
}
for _, u := range remoteConfig.URLs {
if l, p, err := MatchLogins(u, logins); err == nil {
return repo, l, p, nil
}
}
return repo, nil, "", ErrNotAGiteaRepo
}
// MatchLogins matches the given remoteURL against the provided logins and returns
// the first matching login
// remoteURL could be like:
//
// https://gitea.com/owner/repo.git
// http://gitea.com/owner/repo.git
// ssh://gitea.com/owner/repo.git
// git@gitea.com:owner/repo.git
func MatchLogins(remoteURL string, logins []config.Login) (*config.Login, string, error) {
for _, l := range logins {
debug.Printf("Matching remote URL '%s' against %v login", remoteURL, l)
sshHost := l.GetSSHHost()
atIdx := strings.Index(remoteURL, "@")
colonIdx := strings.Index(remoteURL, ":")
if atIdx > 0 && colonIdx > atIdx {
domain := remoteURL[atIdx+1 : colonIdx]
if domain == sshHost {
return &l, strings.TrimSuffix(remoteURL[colonIdx+1:], ".git"), nil
}
} else {
p, err := git.ParseURL(remoteURL)
if err != nil {
return nil, "", fmt.Errorf("git remote URL parse failed: %s", err.Error())
}
switch {
case strings.EqualFold(p.Scheme, "http") || strings.EqualFold(p.Scheme, "https"):
if strings.HasPrefix(remoteURL, l.URL) {
ps := strings.Split(p.Path, "/")
path := strings.Join(ps[len(ps)-2:], "/")
return &l, strings.TrimSuffix(path, ".git"), nil
}
case strings.EqualFold(p.Scheme, "ssh"):
if sshHost == p.Host || sshHost == p.Hostname() {
return &l, strings.TrimLeft(p.Path, "/"), nil
}
default:
// unknown scheme
return nil, "", fmt.Errorf("git remote URL parse failed: %s", "unknown scheme "+p.Scheme)
}
}
}
return nil, "", ErrNotAGiteaRepo
}
// GetLoginByEnvVar returns a login based on environment variables, or nil if no login can be created
func GetLoginByEnvVar() *config.Login {
var token string
giteaToken := os.Getenv("GITEA_TOKEN")
githubToken := os.Getenv("GH_TOKEN")
giteaInstanceURL := os.Getenv("GITEA_INSTANCE_URL")
instanceInsecure := os.Getenv("GITEA_INSTANCE_INSECURE")
insecure := false
if len(instanceInsecure) > 0 {
insecure, _ = strconv.ParseBool(instanceInsecure)
}
// if no tokens are set, or no instance url for gitea fail fast
if len(giteaInstanceURL) == 0 || (len(giteaToken) == 0 && len(githubToken) == 0) {
return nil
}
token = giteaToken
if len(giteaToken) == 0 {
token = githubToken
}
return &config.Login{
Name: "GITEA_LOGIN_VIA_ENV",
URL: giteaInstanceURL,
Token: token,
Insecure: insecure,
SSHKey: "",
SSHCertPrincipal: "",
SSHKeyFingerprint: "",
SSHAgent: false,
Created: time.Now().Unix(),
VersionCheck: false,
}
}