mirror of
https://gitea.com/gitea/tea.git
synced 2024-11-23 19:11:38 +01:00
493b29afbf
The NOTE shown when an explicit login isn't provided is output on Stdout instead of Stderr. This messes up the output of any commands when piping them to a tool such as `yq`. Move this human readable information to Stderr so it doesn't break any automated parsing. Reviewed-on: https://gitea.com/gitea/tea/pulls/636 Co-authored-by: Jake Hillion <jake@hillion.co.uk> Co-committed-by: Jake Hillion <jake@hillion.co.uk>
234 lines
7.2 KiB
Go
234 lines
7.2 KiB
Go
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package context
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
|
|
"code.gitea.io/sdk/gitea"
|
|
"code.gitea.io/tea/modules/config"
|
|
"code.gitea.io/tea/modules/git"
|
|
"code.gitea.io/tea/modules/utils"
|
|
|
|
gogit "github.com/go-git/go-git/v5"
|
|
"github.com/urfave/cli/v2"
|
|
)
|
|
|
|
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.Context
|
|
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
|
|
}
|
|
|
|
// GetListOptions return ListOptions based on PaginationFlags
|
|
func (ctx *TeaContext) GetListOptions() gitea.ListOptions {
|
|
page := ctx.Int("page")
|
|
limit := ctx.Int("limit")
|
|
if limit < 0 {
|
|
limit = 0
|
|
}
|
|
if limit != 0 && page == 0 {
|
|
page = 1
|
|
}
|
|
return gitea.ListOptions{
|
|
Page: page,
|
|
PageSize: limit,
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// InitCommand resolves the application context, and returns the active login, and if
|
|
// available the repo slug. It does this by reading the config file for logins, parsing
|
|
// the remotes of the .git repo specified in repoFlag or $PWD, and using overrides from
|
|
// command flags. If a local git repo can't be found, repo slug values are unset.
|
|
func InitCommand(ctx *cli.Context) *TeaContext {
|
|
// these flags are used as overrides to the context detection via local git repo
|
|
repoFlag := ctx.String("repo")
|
|
loginFlag := ctx.String("login")
|
|
remoteFlag := ctx.String("remote")
|
|
|
|
var (
|
|
c TeaContext
|
|
err error
|
|
repoPath string // empty means PWD
|
|
repoFlagPathExists bool
|
|
)
|
|
|
|
// check if repoFlag can be interpreted as path to local repo.
|
|
if len(repoFlag) != 0 {
|
|
if repoFlagPathExists, err = utils.DirExists(repoFlag); err != nil {
|
|
log.Fatal(err.Error())
|
|
}
|
|
if repoFlagPathExists {
|
|
repoPath = repoFlag
|
|
}
|
|
}
|
|
|
|
if len(remoteFlag) == 0 {
|
|
remoteFlag = config.GetPreferences().FlagDefaults.Remote
|
|
}
|
|
|
|
// try to read local git repo & extract context: if repoFlag specifies a valid path, read repo in that dir,
|
|
// otherwise attempt PWD. if no repo is found, continue with default login
|
|
if c.LocalRepo, c.Login, c.RepoSlug, err = contextFromLocalRepo(repoPath, remoteFlag); err != nil {
|
|
if err == errNotAGiteaRepo || err == gogit.ErrRepositoryNotExists {
|
|
// we can deal with that, commands needing the optional values use ctx.Ensure()
|
|
} else {
|
|
log.Fatal(err.Error())
|
|
}
|
|
}
|
|
|
|
if len(repoFlag) != 0 && !repoFlagPathExists {
|
|
// if repoFlag is not a valid path, use it to override repoSlug
|
|
c.RepoSlug = repoFlag
|
|
}
|
|
|
|
// override login from flag, or use default login if repo based detection failed
|
|
if len(loginFlag) != 0 {
|
|
c.Login = config.GetLoginByName(loginFlag)
|
|
if c.Login == nil {
|
|
log.Fatalf("Login name '%s' does not exist", loginFlag)
|
|
}
|
|
} else if c.Login == nil {
|
|
if c.Login, err = config.GetDefaultLogin(); err != nil {
|
|
if err.Error() == "No available login" {
|
|
// TODO: maybe we can directly start interact.CreateLogin() (only if
|
|
// we're sure we can interactively!), as gh cli does.
|
|
fmt.Println(`No gitea login configured. To start using tea, first run
|
|
tea login add
|
|
and then run your command again.`)
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
fmt.Fprintf(os.Stderr, "NOTE: no gitea login detected, falling back to login '%s'\n", c.Login.Name)
|
|
}
|
|
|
|
// parse reposlug (owner falling back to login owner if reposlug contains only repo name)
|
|
c.Owner, c.Repo = utils.GetOwnerAndRepo(c.RepoSlug, c.Login.User)
|
|
|
|
c.Context = ctx
|
|
c.Output = ctx.String("output")
|
|
return &c
|
|
}
|
|
|
|
// contextFromLocalRepo discovers login & repo slug from the default branch remote of the given local repo
|
|
func contextFromLocalRepo(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
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
logins, err := config.GetLogins()
|
|
if err != nil {
|
|
return repo, nil, "", err
|
|
}
|
|
for _, l := range logins {
|
|
sshHost := l.GetSSHHost()
|
|
for _, u := range remoteConfig.URLs {
|
|
p, err := git.ParseURL(u)
|
|
if err != nil {
|
|
return repo, nil, "", fmt.Errorf("Git remote URL parse failed: %s", err.Error())
|
|
}
|
|
if strings.EqualFold(p.Scheme, "http") || strings.EqualFold(p.Scheme, "https") {
|
|
if strings.HasPrefix(u, l.URL) {
|
|
ps := strings.Split(p.Path, "/")
|
|
path := strings.Join(ps[len(ps)-2:], "/")
|
|
return repo, &l, strings.TrimSuffix(path, ".git"), nil
|
|
}
|
|
} else if strings.EqualFold(p.Scheme, "ssh") {
|
|
if sshHost == p.Host {
|
|
return repo, &l, strings.TrimLeft(p.Path, "/"), nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return repo, nil, "", errNotAGiteaRepo
|
|
}
|