From c2180048a041a5bdccda7254ae84e3e7f7d3f3e7 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Mon, 2 Feb 2026 23:16:39 +0000 Subject: [PATCH] Split up Context (#873) Reviewed-on: https://gitea.com/gitea/tea/pulls/873 Co-authored-by: techknowlogick Co-committed-by: techknowlogick --- modules/context/context.go | 188 ----------------------------- modules/context/context_login.go | 49 ++++++++ modules/context/context_remote.go | 58 +++++++++ modules/context/context_repo.go | 79 ++++++++++++ modules/context/context_require.go | 44 +++++++ 5 files changed, 230 insertions(+), 188 deletions(-) create mode 100644 modules/context/context_login.go create mode 100644 modules/context/context_remote.go create mode 100644 modules/context/context_repo.go create mode 100644 modules/context/context_require.go diff --git a/modules/context/context.go b/modules/context/context.go index 8570041..1fd5c51 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -9,12 +9,8 @@ import ( "log" "os" "path" - "strconv" - "strings" - "time" "code.gitea.io/tea/modules/config" - "code.gitea.io/tea/modules/debug" "code.gitea.io/tea/modules/git" "code.gitea.io/tea/modules/theme" "code.gitea.io/tea/modules/utils" @@ -52,41 +48,6 @@ func (ctx *TeaContext) IsInteractiveMode() bool { return ctx.Command.NumFlags() == 0 } -// 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) - } - - if req.Org && len(ctx.Org) == 0 { - fmt.Println("Organization required: Specify organization via --org.") - os.Exit(1) - } - - if req.Global && !ctx.IsGlobal { - fmt.Println("Global scope required: Specify --global.") - 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 - // ensures ctx.Org is set - Org bool - // ensures ctx.IsGlobal is true - Global 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 @@ -193,152 +154,3 @@ and then run your command again.`) c.Output = cmd.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 - } - 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, - } -} diff --git a/modules/context/context_login.go b/modules/context/context_login.go new file mode 100644 index 0000000..5ebf7cb --- /dev/null +++ b/modules/context/context_login.go @@ -0,0 +1,49 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package context + +import ( + "os" + "strconv" + "time" + + "code.gitea.io/tea/modules/config" +) + +// 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, + } +} diff --git a/modules/context/context_remote.go b/modules/context/context_remote.go new file mode 100644 index 0000000..61dc4bd --- /dev/null +++ b/modules/context/context_remote.go @@ -0,0 +1,58 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package context + +import ( + "fmt" + "strings" + + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/debug" + "code.gitea.io/tea/modules/git" +) + +// 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 +} diff --git a/modules/context/context_repo.go b/modules/context/context_repo.go new file mode 100644 index 0000000..6072d0e --- /dev/null +++ b/modules/context/context_repo.go @@ -0,0 +1,79 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package context + +import ( + "fmt" + + "code.gitea.io/tea/modules/config" + "code.gitea.io/tea/modules/debug" + "code.gitea.io/tea/modules/git" +) + +// 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 + } + 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 +} diff --git a/modules/context/context_require.go b/modules/context/context_require.go new file mode 100644 index 0000000..2e97618 --- /dev/null +++ b/modules/context/context_require.go @@ -0,0 +1,44 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package context + +import ( + "fmt" + "os" +) + +// 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) + } + + if req.Org && len(ctx.Org) == 0 { + fmt.Println("Organization required: Specify organization via --org.") + os.Exit(1) + } + + if req.Global && !ctx.IsGlobal { + fmt.Println("Global scope required: Specify --global.") + 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 + // ensures ctx.Org is set + Org bool + // ensures ctx.IsGlobal is true + Global bool +}