mirror of
https://gitea.com/gitea/tea.git
synced 2026-06-06 03:08:44 +02:00
Merge branch 'main' into lunny/add_reply_code_review
This commit is contained in:
@@ -12,8 +12,8 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"code.gitea.io/tea/modules/httputil"
|
||||
"gitea.dev/tea/modules/config"
|
||||
"gitea.dev/tea/modules/httputil"
|
||||
)
|
||||
|
||||
// Client provides direct HTTP access to Gitea API
|
||||
|
||||
+16
-17
@@ -17,10 +17,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"code.gitea.io/tea/modules/httputil"
|
||||
"code.gitea.io/tea/modules/task"
|
||||
"code.gitea.io/tea/modules/utils"
|
||||
"gitea.dev/tea/modules/config"
|
||||
"gitea.dev/tea/modules/httputil"
|
||||
"gitea.dev/tea/modules/task"
|
||||
"gitea.dev/tea/modules/utils"
|
||||
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"golang.org/x/oauth2"
|
||||
@@ -53,12 +53,12 @@ type OAuthOptions struct {
|
||||
}
|
||||
|
||||
// OAuthLogin performs an OAuth2 PKCE login flow to authorize the CLI
|
||||
func OAuthLogin(name, giteaURL string) error {
|
||||
return OAuthLoginWithOptions(name, giteaURL, false)
|
||||
func OAuthLogin(ctx context.Context, name, giteaURL string) error {
|
||||
return OAuthLoginWithOptions(ctx, name, giteaURL, false)
|
||||
}
|
||||
|
||||
// OAuthLoginWithOptions performs an OAuth2 PKCE login flow with additional options
|
||||
func OAuthLoginWithOptions(name, giteaURL string, insecure bool) error {
|
||||
func OAuthLoginWithOptions(ctx context.Context, name, giteaURL string, insecure bool) error {
|
||||
opts := OAuthOptions{
|
||||
Name: name,
|
||||
URL: giteaURL,
|
||||
@@ -67,22 +67,22 @@ func OAuthLoginWithOptions(name, giteaURL string, insecure bool) error {
|
||||
RedirectURL: fmt.Sprintf("http://%s:%d", redirectHost, redirectPort),
|
||||
Port: redirectPort,
|
||||
}
|
||||
return OAuthLoginWithFullOptions(opts)
|
||||
return OAuthLoginWithFullOptions(ctx, opts)
|
||||
}
|
||||
|
||||
// OAuthLoginWithFullOptions performs an OAuth2 PKCE login flow with full options control
|
||||
func OAuthLoginWithFullOptions(opts OAuthOptions) error {
|
||||
serverURL, token, err := performBrowserOAuthFlow(opts)
|
||||
func OAuthLoginWithFullOptions(ctx context.Context, opts OAuthOptions) error {
|
||||
serverURL, token, err := performBrowserOAuthFlow(ctx, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return createLoginFromToken(opts.Name, serverURL, token, opts.Insecure)
|
||||
return createLoginFromToken(ctx, opts.Name, serverURL, token, opts.Insecure)
|
||||
}
|
||||
|
||||
// performBrowserOAuthFlow performs the browser-based OAuth2 PKCE flow and returns the token.
|
||||
// This is the shared implementation used by both new logins and re-authentication.
|
||||
func performBrowserOAuthFlow(opts OAuthOptions) (serverURL string, token *oauth2.Token, err error) {
|
||||
func performBrowserOAuthFlow(ctx context.Context, opts OAuthOptions) (serverURL string, token *oauth2.Token, err error) {
|
||||
// Normalize URL
|
||||
normalizedURL, err := utils.NormalizeURL(opts.URL)
|
||||
if err != nil {
|
||||
@@ -127,7 +127,6 @@ func performBrowserOAuthFlow(opts OAuthOptions) (serverURL string, token *oauth2
|
||||
codeChallenge := generateCodeChallenge(codeVerifier)
|
||||
|
||||
// Set up the OAuth2 config
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, createHTTPClient(opts.Insecure))
|
||||
|
||||
// Configure the OAuth2 endpoints
|
||||
@@ -366,7 +365,7 @@ func openBrowser(url string) error {
|
||||
}
|
||||
|
||||
// createLoginFromToken creates a login entry using the obtained access token
|
||||
func createLoginFromToken(name, serverURL string, token *oauth2.Token, insecure bool) error {
|
||||
func createLoginFromToken(ctx context.Context, name, serverURL string, token *oauth2.Token, insecure bool) error {
|
||||
if name == "" {
|
||||
var err error
|
||||
name, err = task.GenerateLoginName(serverURL, "")
|
||||
@@ -388,7 +387,7 @@ func createLoginFromToken(name, serverURL string, token *oauth2.Token, insecure
|
||||
|
||||
// Validate token by getting user info
|
||||
client := login.Client()
|
||||
u, _, err := client.GetMyUserInfo()
|
||||
u, _, err := client.Users.GetMyUserInfo(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to validate token: %s", err)
|
||||
}
|
||||
@@ -429,7 +428,7 @@ func RefreshAccessToken(login *config.Login) error {
|
||||
|
||||
// ReauthenticateLogin performs a full browser-based OAuth flow to get new tokens
|
||||
// for an existing login. This is used when the refresh token is expired or invalid.
|
||||
func ReauthenticateLogin(login *config.Login) error {
|
||||
func ReauthenticateLogin(ctx context.Context, login *config.Login) error {
|
||||
opts := OAuthOptions{
|
||||
Name: login.Name,
|
||||
URL: login.URL,
|
||||
@@ -439,7 +438,7 @@ func ReauthenticateLogin(login *config.Login) error {
|
||||
Port: redirectPort,
|
||||
}
|
||||
|
||||
_, token, err := performBrowserOAuthFlow(opts)
|
||||
_, token, err := performBrowserOAuthFlow(ctx, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/tea/modules/utils"
|
||||
"gitea.dev/tea/modules/utils"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/tea/modules/filelock"
|
||||
"gitea.dev/tea/modules/filelock"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -15,11 +15,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/modules/debug"
|
||||
"code.gitea.io/tea/modules/httputil"
|
||||
"code.gitea.io/tea/modules/theme"
|
||||
"code.gitea.io/tea/modules/utils"
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"gitea.dev/tea/modules/debug"
|
||||
"gitea.dev/tea/modules/httputil"
|
||||
"gitea.dev/tea/modules/theme"
|
||||
"gitea.dev/tea/modules/utils"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"golang.org/x/oauth2"
|
||||
@@ -341,7 +342,7 @@ func (l *Login) RefreshOAuthToken() error {
|
||||
}
|
||||
|
||||
// Still need to refresh - proceed with OAuth call
|
||||
newToken, err := doOAuthRefresh(l)
|
||||
newToken, err := doOAuthRefresh(context.Background(), l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -369,7 +370,7 @@ func (l *Login) RefreshOAuthToken() error {
|
||||
}
|
||||
|
||||
// doOAuthRefresh performs the actual OAuth token refresh API call.
|
||||
func doOAuthRefresh(l *Login) (*oauth2.Token, error) {
|
||||
func doOAuthRefresh(ctx context.Context, l *Login) (*oauth2.Token, error) {
|
||||
// Build current token from credstore (single load) or YAML fields
|
||||
var accessToken, refreshToken string
|
||||
var expiry time.Time
|
||||
@@ -388,8 +389,6 @@ func doOAuthRefresh(l *Login) (*oauth2.Token, error) {
|
||||
Expiry: expiry,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: httputil.WrapTransport(&http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: l.Insecure},
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build testtools
|
||||
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
+17
-16
@@ -9,13 +9,12 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"code.gitea.io/tea/modules/git"
|
||||
"code.gitea.io/tea/modules/theme"
|
||||
"code.gitea.io/tea/modules/utils"
|
||||
"gitea.dev/tea/modules/config"
|
||||
"gitea.dev/tea/modules/git"
|
||||
"gitea.dev/tea/modules/theme"
|
||||
"gitea.dev/tea/modules/utils"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
gogit "github.com/go-git/go-git/v5"
|
||||
"github.com/urfave/cli/v3"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
@@ -104,21 +103,23 @@ func InitCommand(cmd *cli.Command) (*TeaContext, error) {
|
||||
|
||||
// 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.RepoSlug == "" {
|
||||
if repoPath == "" {
|
||||
if repoPath, err = os.Getwd(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if repoPath == "" {
|
||||
if repoPath, err = os.Getwd(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if c.LocalRepo, c.Login, c.RepoSlug, err = contextFromLocalRepo(repoPath, remoteFlag, extraLogins); err != nil {
|
||||
if err == errNotAGiteaRepo || err == gogit.ErrRepositoryNotExists {
|
||||
// we can deal with that, commands needing the optional values use ctx.Ensure()
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
var localSlug string
|
||||
if c.LocalRepo, c.Login, localSlug, err = contextFromLocalRepo(repoPath, remoteFlag, extraLogins); err != nil {
|
||||
if err == errNotAGiteaRepo || err == git.ErrRepositoryNotExists {
|
||||
// we can deal with that, commands needing the optional values use ctx.Ensure()
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if c.RepoSlug == "" && localSlug != "" {
|
||||
c.RepoSlug = localSlug
|
||||
}
|
||||
|
||||
// If env vars are set, always use the env login (but repo slug was already
|
||||
// resolved by contextFromLocalRepo with the env login in the match list)
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"gitea.dev/tea/modules/config"
|
||||
)
|
||||
|
||||
// GetLoginByEnvVar returns a login based on environment variables, or nil if no login can be created
|
||||
|
||||
@@ -6,7 +6,7 @@ package context
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"gitea.dev/tea/modules/config"
|
||||
)
|
||||
|
||||
func TestShouldPromptFallbackLogin(t *testing.T) {
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"code.gitea.io/tea/modules/debug"
|
||||
"code.gitea.io/tea/modules/git"
|
||||
"gitea.dev/tea/modules/config"
|
||||
"gitea.dev/tea/modules/debug"
|
||||
"gitea.dev/tea/modules/git"
|
||||
)
|
||||
|
||||
// MatchLogins matches the given remoteURL against the provided logins and returns
|
||||
|
||||
@@ -6,9 +6,9 @@ package context
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"code.gitea.io/tea/modules/debug"
|
||||
"code.gitea.io/tea/modules/git"
|
||||
"gitea.dev/tea/modules/config"
|
||||
"gitea.dev/tea/modules/debug"
|
||||
"gitea.dev/tea/modules/git"
|
||||
)
|
||||
|
||||
// contextFromLocalRepo discovers login & repo slug from the default branch remote of the given local repo
|
||||
|
||||
@@ -6,10 +6,11 @@ package context
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"code.gitea.io/tea/modules/git"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"gitea.dev/tea/modules/config"
|
||||
"gitea.dev/tea/modules/git"
|
||||
)
|
||||
|
||||
func TestEnsureReturnsRequirementErrors(t *testing.T) {
|
||||
|
||||
@@ -6,7 +6,7 @@ package context
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"gitea.dev/tea/modules/config"
|
||||
)
|
||||
|
||||
func Test_MatchLogins(t *testing.T) {
|
||||
|
||||
+44
-49
@@ -7,69 +7,64 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/tea/modules/utils"
|
||||
|
||||
git_transport "github.com/go-git/go-git/v5/plumbing/transport"
|
||||
gogit_http "github.com/go-git/go-git/v5/plumbing/transport/http"
|
||||
gogit_ssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
|
||||
"gitea.dev/tea/modules/utils"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type pwCallback = func(string) (string, error)
|
||||
|
||||
// GetAuthForURL returns the appropriate AuthMethod to be used in Push() / Pull()
|
||||
// operations depending on the protocol, and prompts the user for credentials if
|
||||
// necessary.
|
||||
func GetAuthForURL(remoteURL *url.URL, authToken, keyFile string, passwordCallback pwCallback) (git_transport.AuthMethod, error) {
|
||||
// GetAuthForURL returns backend-agnostic auth settings for git network operations.
|
||||
func GetAuthForURL(remoteURL *url.URL, authToken, keyFile string, passwordCallback pwCallback) (*AuthMethod, error) {
|
||||
switch remoteURL.Scheme {
|
||||
case "http", "https":
|
||||
// gitea supports push/pull via app token as username.
|
||||
return &gogit_http.BasicAuth{Password: "", Username: authToken}, nil
|
||||
|
||||
return &AuthMethod{Scheme: remoteURL.Scheme, Username: authToken}, nil
|
||||
case "ssh":
|
||||
// try to select right key via ssh-agent. if it fails, try to read a key manually
|
||||
user := remoteURL.User.Username()
|
||||
auth, err := gogit_ssh.DefaultAuthBuilder(user)
|
||||
if err != nil {
|
||||
signer, err2 := readSSHPrivKey(keyFile, passwordCallback)
|
||||
if err2 != nil {
|
||||
return nil, err2
|
||||
}
|
||||
auth = &gogit_ssh.PublicKeys{User: user, Signer: signer}
|
||||
if keyFile == "" {
|
||||
return &AuthMethod{Scheme: remoteURL.Scheme, Username: remoteURL.User.Username()}, nil
|
||||
}
|
||||
expandedKeyFile, err := utils.AbsPathWithExpansion(keyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sshKey, err := os.ReadFile(expandedKeyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can not read ssh key '%s'", expandedKeyFile)
|
||||
}
|
||||
return auth, nil
|
||||
}
|
||||
return nil, fmt.Errorf("don't know how to handle url scheme %v", remoteURL.Scheme)
|
||||
}
|
||||
|
||||
func readSSHPrivKey(keyFile string, passwordCallback pwCallback) (sig ssh.Signer, err error) {
|
||||
if keyFile != "" {
|
||||
keyFile, err = utils.AbsPathWithExpansion(keyFile)
|
||||
} else {
|
||||
keyFile, err = utils.AbsPathWithExpansion("~/.ssh/id_rsa")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sshKey, err := os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can not read ssh key '%s'", keyFile)
|
||||
}
|
||||
sig, err = ssh.ParsePrivateKey(sshKey)
|
||||
if _, ok := err.(*ssh.PassphraseMissingError); ok && passwordCallback != nil {
|
||||
// allow for up to 3 password attempts
|
||||
for i := 0; i < 3; i++ {
|
||||
var pass string
|
||||
pass, err = passwordCallback(keyFile)
|
||||
if err != nil {
|
||||
auth := &AuthMethod{
|
||||
Scheme: remoteURL.Scheme,
|
||||
Username: remoteURL.User.Username(),
|
||||
KeyFile: expandedKeyFile,
|
||||
}
|
||||
if _, err := ssh.ParsePrivateKey(sshKey); err == nil {
|
||||
return auth, nil
|
||||
}
|
||||
|
||||
if _, ok := err.(*ssh.PassphraseMissingError); ok {
|
||||
if passwordCallback == nil {
|
||||
return nil, err
|
||||
}
|
||||
sig, err = ssh.ParsePrivateKeyWithPassphrase(sshKey, []byte(pass))
|
||||
if err == nil {
|
||||
break
|
||||
for i := 0; i < 3; i++ {
|
||||
pass, err := passwordCallback(expandedKeyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := ssh.ParsePrivateKeyWithPassphrase(sshKey, []byte(pass)); err == nil {
|
||||
auth.KeyPassphrase = pass
|
||||
return auth, nil
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil, err
|
||||
default:
|
||||
return nil, fmt.Errorf("don't know how to handle url scheme %v", remoteURL.Scheme)
|
||||
}
|
||||
return sig, err
|
||||
}
|
||||
|
||||
func shellQuote(s string) string {
|
||||
return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var backendRegistry = struct {
|
||||
sync.RWMutex
|
||||
current string
|
||||
backends map[string]Backend
|
||||
}{
|
||||
backends: make(map[string]Backend),
|
||||
}
|
||||
|
||||
func init() {
|
||||
mustRegisterBackend(cliBackend{})
|
||||
mustUseBackend("cli")
|
||||
}
|
||||
|
||||
// RegisterBackend makes a git backend available for later switching.
|
||||
func RegisterBackend(backend Backend) error {
|
||||
if backend == nil {
|
||||
return fmt.Errorf("git backend is nil")
|
||||
}
|
||||
name := backend.Name()
|
||||
if name == "" {
|
||||
return fmt.Errorf("git backend name is empty")
|
||||
}
|
||||
|
||||
backendRegistry.Lock()
|
||||
defer backendRegistry.Unlock()
|
||||
backendRegistry.backends[name] = backend
|
||||
if backendRegistry.current == "" {
|
||||
backendRegistry.current = name
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mustRegisterBackend(backend Backend) {
|
||||
if err := RegisterBackend(backend); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// UseBackend switches the active git backend implementation.
|
||||
func UseBackend(name string) error {
|
||||
backendRegistry.Lock()
|
||||
defer backendRegistry.Unlock()
|
||||
if _, ok := backendRegistry.backends[name]; !ok {
|
||||
return fmt.Errorf("git backend %q is not registered", name)
|
||||
}
|
||||
backendRegistry.current = name
|
||||
return nil
|
||||
}
|
||||
|
||||
func mustUseBackend(name string) {
|
||||
if err := UseBackend(name); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// CurrentBackendName returns the active backend name.
|
||||
func CurrentBackendName() string {
|
||||
backendRegistry.RLock()
|
||||
defer backendRegistry.RUnlock()
|
||||
return backendRegistry.current
|
||||
}
|
||||
|
||||
// RegisteredBackends returns all registered backend names.
|
||||
func RegisteredBackends() []string {
|
||||
backendRegistry.RLock()
|
||||
defer backendRegistry.RUnlock()
|
||||
out := make([]string, 0, len(backendRegistry.backends))
|
||||
for name := range backendRegistry.backends {
|
||||
out = append(out, name)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func currentBackend() Backend {
|
||||
backendRegistry.RLock()
|
||||
defer backendRegistry.RUnlock()
|
||||
return backendRegistry.backends[backendRegistry.current]
|
||||
}
|
||||
|
||||
func setBackendForTesting(t testingT, backend Backend) {
|
||||
t.Helper()
|
||||
prev := CurrentBackendName()
|
||||
mustRegisterBackend(backend)
|
||||
mustUseBackend(backend.Name())
|
||||
t.Cleanup(func() { mustUseBackend(prev) })
|
||||
}
|
||||
|
||||
type testingT interface {
|
||||
Cleanup(func())
|
||||
Helper()
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type fakeBackend struct{}
|
||||
|
||||
type fakeRepoBackend struct {
|
||||
workTree string
|
||||
}
|
||||
|
||||
func (fakeBackend) Name() string { return "fake" }
|
||||
|
||||
func (fakeBackend) Open(path string) (RepositoryBackend, error) {
|
||||
return &fakeRepoBackend{workTree: "open:" + path}, nil
|
||||
}
|
||||
|
||||
func (fakeBackend) Clone(path, remoteURL string, auth *AuthMethod, opts CloneOptions) (RepositoryBackend, error) {
|
||||
return &fakeRepoBackend{workTree: fmt.Sprintf("clone:%s:%s", path, remoteURL)}, nil
|
||||
}
|
||||
|
||||
func (r *fakeRepoBackend) WorkTree() string { return r.workTree }
|
||||
func (r *fakeRepoBackend) Config() (*Config, error) {
|
||||
return &Config{Remotes: map[string]*RemoteConfig{}, Branches: map[string]*Branch{}}, nil
|
||||
}
|
||||
|
||||
func (r *fakeRepoBackend) Head() (*Reference, error) {
|
||||
return &Reference{name: NewBranchReferenceName("main"), hash: Hash("deadbeef")}, nil
|
||||
}
|
||||
func (r *fakeRepoBackend) AddRemote(name, remoteURL string) error { return nil }
|
||||
func (r *fakeRepoBackend) SetBranchUpstream(branchName, remoteName, remoteBranch string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *fakeRepoBackend) Fetch(remoteName string, refspecs []string, auth *AuthMethod) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *fakeRepoBackend) CreateTrackingBranch(localBranchName, remoteBranchName, remoteName string) error {
|
||||
return nil
|
||||
}
|
||||
func (r *fakeRepoBackend) Checkout(ref ReferenceName) error { return nil }
|
||||
func (r *fakeRepoBackend) DeleteLocalBranch(branchName string) error { return nil }
|
||||
func (r *fakeRepoBackend) DeleteRemoteBranch(remoteName, remoteBranch string, auth *AuthMethod) error {
|
||||
return nil
|
||||
}
|
||||
func (r *fakeRepoBackend) ListReferences(prefixes ...string) ([]*Reference, error) { return nil, nil }
|
||||
func (r *fakeRepoBackend) PushToAgitFlowPR(remoteName, head, base, topic string, pushOptions map[string]string, auth *AuthMethod) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCanSwitchBackends(t *testing.T) {
|
||||
setBackendForTesting(t, fakeBackend{})
|
||||
|
||||
repo, err := RepoFromPath("demo")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "open:demo", repo.WorkTree())
|
||||
require.Equal(t, "fake", CurrentBackendName())
|
||||
|
||||
cloned, err := Clone("target", "https://example.com/repo.git", nil, 1, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "clone:target:https://example.com/repo.git", cloned.WorkTree())
|
||||
}
|
||||
|
||||
func TestRegisteredBackendsContainsCLI(t *testing.T) {
|
||||
require.Contains(t, RegisteredBackends(), "cli")
|
||||
}
|
||||
+74
-155
@@ -6,71 +6,33 @@ package git
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
git_config "github.com/go-git/go-git/v5/config"
|
||||
git_plumbing "github.com/go-git/go-git/v5/plumbing"
|
||||
git_transport "github.com/go-git/go-git/v5/plumbing/transport"
|
||||
)
|
||||
|
||||
// TeaCreateBranch creates a new branch in the repo, tracking from another branch.
|
||||
func (r TeaRepo) TeaCreateBranch(localBranchName, remoteBranchName, remoteName string) error {
|
||||
localBranchRefName := git_plumbing.NewBranchReferenceName(localBranchName)
|
||||
if _, err := r.Reference(localBranchRefName, true); err == nil {
|
||||
return git.ErrBranchExists
|
||||
} else if err != nil && err != git_plumbing.ErrReferenceNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
return runGitCommand("branch", "--track", localBranchName, fmt.Sprintf("%s/%s", remoteName, remoteBranchName))
|
||||
return r.backend.CreateTrackingBranch(localBranchName, remoteBranchName, remoteName)
|
||||
}
|
||||
|
||||
// TeaCheckout checks out the given branch in the worktree.
|
||||
func (r TeaRepo) TeaCheckout(ref git_plumbing.ReferenceName) error {
|
||||
args := []string{"checkout"}
|
||||
if ref.IsRemote() {
|
||||
args = append(args, "--detach", ref.String())
|
||||
} else if ref.IsBranch() {
|
||||
args = append(args, ref.Short())
|
||||
} else {
|
||||
args = append(args, ref.String())
|
||||
}
|
||||
|
||||
return runGitCommand(args...)
|
||||
func (r TeaRepo) TeaCheckout(ref ReferenceName) error {
|
||||
return r.backend.Checkout(ref)
|
||||
}
|
||||
|
||||
// TeaDeleteLocalBranch removes the given branch locally
|
||||
func (r TeaRepo) TeaDeleteLocalBranch(branch *git_config.Branch) error {
|
||||
err := r.DeleteBranch(branch.Name)
|
||||
// if the branch is not found that's ok, as .git/config may have no entry if
|
||||
// no remote tracking branch is configured for it (eg push without -u flag)
|
||||
if err != nil && err.Error() != "branch not found" {
|
||||
return err
|
||||
}
|
||||
return r.Storer.RemoveReference(git_plumbing.NewBranchReferenceName(branch.Name))
|
||||
// TeaDeleteLocalBranch removes the given branch locally.
|
||||
func (r TeaRepo) TeaDeleteLocalBranch(branch *Branch) error {
|
||||
return r.backend.DeleteLocalBranch(branch.Name)
|
||||
}
|
||||
|
||||
// TeaDeleteRemoteBranch removes the given branch on the given remote via git protocol
|
||||
func (r TeaRepo) TeaDeleteRemoteBranch(remoteName, remoteBranch string, auth git_transport.AuthMethod) error {
|
||||
// delete remote branch via git protocol:
|
||||
// an empty source in the refspec means remote deletion to git 🙃
|
||||
refspec := fmt.Sprintf(":%s", git_plumbing.NewBranchReferenceName(remoteBranch))
|
||||
return r.Push(&git.PushOptions{
|
||||
RemoteName: remoteName,
|
||||
RefSpecs: []git_config.RefSpec{git_config.RefSpec(refspec)},
|
||||
Prune: true,
|
||||
Auth: auth,
|
||||
})
|
||||
// TeaDeleteRemoteBranch removes the given branch on the given remote via git protocol.
|
||||
func (r TeaRepo) TeaDeleteRemoteBranch(remoteName, remoteBranch string, auth *AuthMethod) error {
|
||||
return r.backend.DeleteRemoteBranch(remoteName, remoteBranch, auth)
|
||||
}
|
||||
|
||||
// TeaFindBranchBySha returns a branch that is at the the given SHA and syncs to the
|
||||
// given remote repo.
|
||||
func (r TeaRepo) TeaFindBranchBySha(sha, repoURL string) (b *git_config.Branch, err error) {
|
||||
// find remote matching our repoURL
|
||||
func (r TeaRepo) TeaFindBranchBySha(sha, repoURL string) (b *Branch, err error) {
|
||||
remote, err := r.GetRemote(repoURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -80,41 +42,26 @@ func (r TeaRepo) TeaFindBranchBySha(sha, repoURL string) (b *git_config.Branch,
|
||||
}
|
||||
remoteName := remote.Config().Name
|
||||
|
||||
// check if the given remote has our branch (.git/refs/remotes/<remoteName>/*)
|
||||
iter, err := r.References()
|
||||
refs, err := r.backend.ListReferences("refs/heads", "refs/remotes/"+remoteName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer iter.Close()
|
||||
var remoteRefName git_plumbing.ReferenceName
|
||||
var localRefName git_plumbing.ReferenceName
|
||||
err = iter.ForEach(func(ref *git_plumbing.Reference) error {
|
||||
if ref.Name().IsRemote() {
|
||||
name := ref.Name().Short()
|
||||
if ref.Hash().String() == sha && strings.HasPrefix(name, remoteName) {
|
||||
remoteRefName = ref.Name()
|
||||
}
|
||||
}
|
||||
|
||||
var remoteRefName ReferenceName
|
||||
var localRefName ReferenceName
|
||||
for _, ref := range refs {
|
||||
if ref.Name().IsRemote() && ref.Hash().String() == sha {
|
||||
remoteRefName = ref.Name()
|
||||
}
|
||||
if ref.Name().IsBranch() && ref.Hash().String() == sha {
|
||||
localRefName = ref.Name()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if remoteRefName == "" || localRefName == "" {
|
||||
// no remote tracking branch found, so a potential local branch
|
||||
// can't be a match either
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b = &git_config.Branch{
|
||||
Remote: remoteName,
|
||||
Name: localRefName.Short(),
|
||||
Merge: localRefName,
|
||||
}
|
||||
b = &Branch{Remote: remoteName, Name: localRefName.Short(), Merge: localRefName}
|
||||
return b, b.Validate()
|
||||
}
|
||||
|
||||
@@ -122,8 +69,7 @@ func (r TeaRepo) TeaFindBranchBySha(sha, repoURL string) (b *git_config.Branch,
|
||||
// remote names and syncs to the given remote repo. This method is less precise
|
||||
// than TeaFindBranchBySha(), but may be desirable if local and remote branch
|
||||
// have diverged.
|
||||
func (r TeaRepo) TeaFindBranchByName(branchName, repoURL string) (b *git_config.Branch, err error) {
|
||||
// find remote matching our repoURL
|
||||
func (r TeaRepo) TeaFindBranchByName(branchName, repoURL string) (b *Branch, err error) {
|
||||
remote, err := r.GetRemote(repoURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -133,45 +79,35 @@ func (r TeaRepo) TeaFindBranchByName(branchName, repoURL string) (b *git_config.
|
||||
}
|
||||
remoteName := remote.Config().Name
|
||||
|
||||
// check if the given remote has our branch (.git/refs/remotes/<remoteName>/*)
|
||||
iter, err := r.References()
|
||||
refs, err := r.backend.ListReferences("refs/heads", "refs/remotes/"+remoteName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer iter.Close()
|
||||
var remoteRefName git_plumbing.ReferenceName
|
||||
var localRefName git_plumbing.ReferenceName
|
||||
|
||||
var remoteRefName ReferenceName
|
||||
var localRefName ReferenceName
|
||||
remoteSearchingName := fmt.Sprintf("%s/%s", remoteName, branchName)
|
||||
err = iter.ForEach(func(ref *git_plumbing.Reference) error {
|
||||
for _, ref := range refs {
|
||||
if ref.Name().IsRemote() && ref.Name().Short() == remoteSearchingName {
|
||||
remoteRefName = ref.Name()
|
||||
}
|
||||
n := ref.Name()
|
||||
if n.IsBranch() && n.Short() == branchName {
|
||||
localRefName = n
|
||||
if ref.Name().IsBranch() && ref.Name().Short() == branchName {
|
||||
localRefName = ref.Name()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if remoteRefName == "" || localRefName == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
b = &git_config.Branch{
|
||||
Remote: remoteName,
|
||||
Name: localRefName.Short(),
|
||||
Merge: localRefName,
|
||||
}
|
||||
b = &Branch{Remote: remoteName, Name: localRefName.Short(), Merge: localRefName}
|
||||
return b, b.Validate()
|
||||
}
|
||||
|
||||
// TeaFindBranchRemote gives the first remote that has a branch with the same name or sha,
|
||||
// depending on what is passed in.
|
||||
// This function is needed, as git does not always define branches in .git/config with remote entries.
|
||||
// Priority order is: first match of sha and branch -> first match of branch -> first match of sha
|
||||
func (r TeaRepo) TeaFindBranchRemote(branchName, hash string) (*git.Remote, error) {
|
||||
// Priority order is: first match of sha and branch -> first match of branch -> first match of sha.
|
||||
func (r TeaRepo) TeaFindBranchRemote(branchName, hash string) (*Remote, error) {
|
||||
remotes, err := r.Remotes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -184,55 +120,53 @@ func (r TeaRepo) TeaFindBranchRemote(branchName, hash string) (*git.Remote, erro
|
||||
return remotes[0], nil
|
||||
}
|
||||
|
||||
// check if the given remote has our branch (.git/refs/remotes/<remoteName>/*)
|
||||
iter, err := r.References()
|
||||
refs, err := r.backend.ListReferences("refs/remotes")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer iter.Close()
|
||||
|
||||
var shaMatch *git.Remote
|
||||
var branchMatch *git.Remote
|
||||
var fullMatch *git.Remote
|
||||
if err := iter.ForEach(func(ref *git_plumbing.Reference) error {
|
||||
if ref.Name().IsRemote() {
|
||||
names := strings.SplitN(ref.Name().Short(), "/", 2)
|
||||
remote := names[0]
|
||||
branch := names[1]
|
||||
if branchMatch == nil && branchName != "" && branchName == branch {
|
||||
if branchMatch, err = r.Remote(remote); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if shaMatch == nil && hash != "" && hash == ref.Hash().String() {
|
||||
if shaMatch, err = r.Remote(remote); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if fullMatch == nil && branchName != "" && branchName == branch && hash != "" && hash == ref.Hash().String() {
|
||||
if fullMatch, err = r.Remote(remote); err != nil {
|
||||
return err
|
||||
}
|
||||
// stop asap you have a full match
|
||||
return nil
|
||||
}
|
||||
remoteByName := make(map[string]*Remote, len(remotes))
|
||||
for _, remote := range remotes {
|
||||
remoteByName[remote.Config().Name] = remote
|
||||
}
|
||||
|
||||
var shaMatch *Remote
|
||||
var branchMatch *Remote
|
||||
var fullMatch *Remote
|
||||
for _, ref := range refs {
|
||||
remoteName, remoteBranch, ok := splitRemoteRef(ref.Name())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
remote := remoteByName[remoteName]
|
||||
if remote == nil {
|
||||
continue
|
||||
}
|
||||
if branchMatch == nil && branchName != "" && branchName == remoteBranch {
|
||||
branchMatch = remote
|
||||
}
|
||||
if shaMatch == nil && hash != "" && hash == ref.Hash().String() {
|
||||
shaMatch = remote
|
||||
}
|
||||
if fullMatch == nil && branchName != "" && branchName == remoteBranch && hash != "" && hash == ref.Hash().String() {
|
||||
fullMatch = remote
|
||||
break
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if fullMatch != nil {
|
||||
switch {
|
||||
case fullMatch != nil:
|
||||
return fullMatch, nil
|
||||
} else if branchMatch != nil {
|
||||
case branchMatch != nil:
|
||||
return branchMatch, nil
|
||||
} else if shaMatch != nil {
|
||||
case shaMatch != nil:
|
||||
return shaMatch, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// TeaGetCurrentBranchNameAndSHA return the name and sha of the branch witch is currently active
|
||||
// TeaGetCurrentBranchNameAndSHA return the name and sha of the branch witch is currently active.
|
||||
func (r TeaRepo) TeaGetCurrentBranchNameAndSHA() (string, string, error) {
|
||||
localHead, err := r.Head()
|
||||
if err != nil {
|
||||
@@ -247,13 +181,11 @@ func (r TeaRepo) TeaGetCurrentBranchNameAndSHA() (string, string, error) {
|
||||
}
|
||||
|
||||
// PushToCreatAgitFlowPR pushes the given head to the refs/for/<base>/<topic> ref on the remote to create an agit flow PR.
|
||||
func (r TeaRepo) PushToCreatAgitFlowPR(remoteName, head, base, topic, title, description string, auth git_transport.AuthMethod) error {
|
||||
func (r TeaRepo) PushToCreatAgitFlowPR(remoteName, head, base, topic, title, description string, auth *AuthMethod) error {
|
||||
if !strings.HasPrefix(head, "refs/") {
|
||||
head = "refs/heads/" + head
|
||||
}
|
||||
|
||||
ref := fmt.Sprintf("%s:refs/for/%s/%s", head, base, topic)
|
||||
|
||||
pushOptions := make(map[string]string)
|
||||
if len(title) > 0 {
|
||||
pushOptions["title"] = b64Encode(title)
|
||||
@@ -261,15 +193,18 @@ func (r TeaRepo) PushToCreatAgitFlowPR(remoteName, head, base, topic, title, des
|
||||
if len(description) > 0 {
|
||||
pushOptions["description"] = b64Encode(description)
|
||||
}
|
||||
return r.backend.PushToAgitFlowPR(remoteName, head, base, topic, pushOptions, auth)
|
||||
}
|
||||
|
||||
opts := &git.PushOptions{
|
||||
RemoteName: remoteName,
|
||||
RefSpecs: []git_config.RefSpec{git_config.RefSpec(ref)},
|
||||
Options: pushOptions,
|
||||
Auth: auth,
|
||||
func splitRemoteRef(ref ReferenceName) (remoteName, branchName string, ok bool) {
|
||||
if !ref.IsRemote() {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
return r.Push(opts)
|
||||
parts := strings.SplitN(ref.Short(), "/", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", "", false
|
||||
}
|
||||
return parts[0], parts[1], true
|
||||
}
|
||||
|
||||
// b64Encode implements base64 encode for string if necessary.
|
||||
@@ -289,19 +224,3 @@ func isASCII(s string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func runGitCommand(args ...string) error {
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Env = os.Environ()
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
msg := strings.TrimSpace(string(output))
|
||||
if msg == "" {
|
||||
return fmt.Errorf("git %s: %w", strings.Join(args, " "), err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("git %s: %w: %s", strings.Join(args, " "), err, msg)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type cliBackend struct{}
|
||||
|
||||
type cliRepository struct {
|
||||
workTree string
|
||||
}
|
||||
|
||||
func (cliBackend) Name() string {
|
||||
return "cli"
|
||||
}
|
||||
|
||||
func (b cliBackend) Open(path string) (RepositoryBackend, error) {
|
||||
if path == "" {
|
||||
path = "."
|
||||
}
|
||||
|
||||
out, err := runGitCommand(path, nil, nil, "rev-parse", "--show-toplevel")
|
||||
if err != nil {
|
||||
return nil, classifyRepoError(err)
|
||||
}
|
||||
|
||||
return &cliRepository{workTree: strings.TrimSpace(out)}, nil
|
||||
}
|
||||
|
||||
func (b cliBackend) Clone(path, remoteURL string, auth *AuthMethod, opts CloneOptions) (RepositoryBackend, error) {
|
||||
extraConfigs := make([]string, 0, 1)
|
||||
if opts.Insecure {
|
||||
extraConfigs = append(extraConfigs, "http.sslVerify=false")
|
||||
}
|
||||
|
||||
args := []string{"clone"}
|
||||
if opts.Depth > 0 {
|
||||
args = append(args, "--depth", strconv.Itoa(opts.Depth))
|
||||
}
|
||||
args = append(args, remoteURL, path)
|
||||
|
||||
if _, err := runGitCommand("", auth, extraConfigs, args...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.Open(path)
|
||||
}
|
||||
|
||||
func (r *cliRepository) WorkTree() string {
|
||||
return r.workTree
|
||||
}
|
||||
|
||||
func (r *cliRepository) Config() (*Config, error) {
|
||||
cfg := &Config{
|
||||
Remotes: map[string]*RemoteConfig{},
|
||||
Branches: map[string]*Branch{},
|
||||
}
|
||||
|
||||
remoteOut, err := r.git(nil, nil, "remote")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, remoteName := range strings.Fields(remoteOut) {
|
||||
urlOut, err := r.git(nil, nil, "config", "--get-all", "remote."+remoteName+".url")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.Remotes[remoteName] = &RemoteConfig{Name: remoteName, URLs: splitNonEmptyLines(urlOut)}
|
||||
}
|
||||
|
||||
branchOut, err := r.git(nil, nil, "config", "--get-regexp", `^branch\..*\.(remote|merge)$`)
|
||||
if err != nil {
|
||||
var gitErr *gitCommandError
|
||||
if !errors.As(err, &gitErr) {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(gitErr.stderr) != "" && !strings.Contains(gitErr.stderr, "No such section or key") {
|
||||
return nil, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
for _, line := range splitNonEmptyLines(branchOut) {
|
||||
parts := strings.SplitN(line, " ", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
branchName, field, ok := parseBranchConfigKey(parts[0])
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
branch := cfg.Branches[branchName]
|
||||
if branch == nil {
|
||||
branch = &Branch{Name: branchName}
|
||||
cfg.Branches[branchName] = branch
|
||||
}
|
||||
switch field {
|
||||
case "remote":
|
||||
branch.Remote = parts[1]
|
||||
case "merge":
|
||||
branch.Merge = ReferenceName(parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (r *cliRepository) Head() (*Reference, error) {
|
||||
hashOut, err := r.git(nil, nil, "rev-parse", "HEAD")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hash := Hash(strings.TrimSpace(hashOut))
|
||||
|
||||
if refOut, err := r.git(nil, nil, "symbolic-ref", "-q", "HEAD"); err == nil {
|
||||
return &Reference{name: ReferenceName(strings.TrimSpace(refOut)), hash: hash}, nil
|
||||
}
|
||||
if tagOut, err := r.git(nil, nil, "describe", "--exact-match", "--tags", "HEAD"); err == nil {
|
||||
return &Reference{name: ReferenceName("refs/tags/" + strings.TrimSpace(tagOut)), hash: hash}, nil
|
||||
}
|
||||
return &Reference{name: ReferenceName("HEAD"), hash: hash}, nil
|
||||
}
|
||||
|
||||
func (r *cliRepository) AddRemote(name, remoteURL string) error {
|
||||
_, err := r.git(nil, nil, "remote", "add", name, remoteURL)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *cliRepository) SetBranchUpstream(branchName, remoteName, remoteBranch string) error {
|
||||
mergeRef := NewBranchReferenceName(remoteBranch).String()
|
||||
if _, err := r.git(nil, nil, "config", "branch."+branchName+".remote", remoteName); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := r.git(nil, nil, "config", "branch."+branchName+".merge", mergeRef)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *cliRepository) Fetch(remoteName string, refspecs []string, auth *AuthMethod) error {
|
||||
args := []string{"fetch", remoteName}
|
||||
args = append(args, refspecs...)
|
||||
_, err := r.git(auth, nil, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *cliRepository) CreateTrackingBranch(localBranchName, remoteBranchName, remoteName string) error {
|
||||
_, err := r.git(nil, nil, "branch", "--track", localBranchName, fmt.Sprintf("%s/%s", remoteName, remoteBranchName))
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if gitErr, ok := err.(*gitCommandError); ok && strings.Contains(gitErr.stderr, "already exists") {
|
||||
return ErrBranchExists
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *cliRepository) Checkout(ref ReferenceName) error {
|
||||
_, err := r.git(nil, nil, "checkout", ref.String())
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *cliRepository) DeleteLocalBranch(branchName string) error {
|
||||
_, err := r.git(nil, nil, "branch", "-D", branchName)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if gitErr, ok := err.(*gitCommandError); ok {
|
||||
stderr := strings.ToLower(gitErr.stderr)
|
||||
if strings.Contains(stderr, "not found") || strings.Contains(stderr, "not exist") {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *cliRepository) DeleteRemoteBranch(remoteName, remoteBranch string, auth *AuthMethod) error {
|
||||
_, err := r.git(auth, nil, "push", "--delete", remoteName, remoteBranch)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *cliRepository) ListReferences(prefixes ...string) ([]*Reference, error) {
|
||||
args := []string{"for-each-ref", "--format=%(objectname)%09%(refname)"}
|
||||
args = append(args, prefixes...)
|
||||
out, err := r.git(nil, nil, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refs := make([]*Reference, 0)
|
||||
for _, line := range splitNonEmptyLines(out) {
|
||||
parts := strings.SplitN(line, " ", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
refs = append(refs, &Reference{name: ReferenceName(parts[1]), hash: Hash(parts[0])})
|
||||
}
|
||||
return refs, nil
|
||||
}
|
||||
|
||||
func (r *cliRepository) PushToAgitFlowPR(remoteName, head, base, topic string, pushOptions map[string]string, auth *AuthMethod) error {
|
||||
ref := fmt.Sprintf("%s:refs/for/%s/%s", head, base, topic)
|
||||
args := []string{"push", remoteName, ref}
|
||||
if len(pushOptions) > 0 {
|
||||
keys := make([]string, 0, len(pushOptions))
|
||||
for key := range pushOptions {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
args = append(args, "-o", key+"="+pushOptions[key])
|
||||
}
|
||||
}
|
||||
_, err := r.git(auth, nil, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *cliRepository) git(auth *AuthMethod, extraConfigs []string, args ...string) (string, error) {
|
||||
return runGitCommand(r.workTree, auth, extraConfigs, args...)
|
||||
}
|
||||
|
||||
type gitCommandError struct {
|
||||
args []string
|
||||
stderr string
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *gitCommandError) Error() string {
|
||||
stderr := strings.TrimSpace(e.stderr)
|
||||
if stderr == "" {
|
||||
return fmt.Sprintf("git %s: %v", strings.Join(e.args, " "), e.err)
|
||||
}
|
||||
return fmt.Sprintf("git %s: %s", strings.Join(e.args, " "), stderr)
|
||||
}
|
||||
|
||||
func (e *gitCommandError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func runGitCommand(dir string, auth *AuthMethod, extraConfigs []string, args ...string) (string, error) {
|
||||
authConfigs, authEnv, cleanup, err := prepareCLIAuth(auth)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
fullArgs := make([]string, 0, (len(extraConfigs)+len(authConfigs))*2+len(args))
|
||||
for _, cfg := range extraConfigs {
|
||||
fullArgs = append(fullArgs, "-c", cfg)
|
||||
}
|
||||
for _, cfg := range authConfigs {
|
||||
fullArgs = append(fullArgs, "-c", cfg)
|
||||
}
|
||||
fullArgs = append(fullArgs, args...)
|
||||
|
||||
cmd := exec.Command("git", fullArgs...)
|
||||
if dir != "" {
|
||||
cmd.Dir = dir
|
||||
}
|
||||
cmd.Env = append(os.Environ(), authEnv...)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", &gitCommandError{args: fullArgs, stderr: stderr.String(), err: err}
|
||||
}
|
||||
return stdout.String(), nil
|
||||
}
|
||||
|
||||
func prepareCLIAuth(auth *AuthMethod) ([]string, []string, func(), error) {
|
||||
if auth == nil {
|
||||
return nil, nil, func() {}, nil
|
||||
}
|
||||
|
||||
configs := make([]string, 0, 1)
|
||||
env := make([]string, 0, 4)
|
||||
cleanup := func() {}
|
||||
|
||||
switch auth.Scheme {
|
||||
case "http", "https":
|
||||
if auth.Username != "" || auth.Password != "" {
|
||||
header := "Authorization: Basic " + base64.StdEncoding.EncodeToString([]byte(auth.Username+":"+auth.Password))
|
||||
configs = append(configs, "http.extraHeader="+header)
|
||||
}
|
||||
case "ssh":
|
||||
sshCommand := "ssh"
|
||||
if auth.KeyFile != "" {
|
||||
sshCommand += " -i " + shellQuote(auth.KeyFile) + " -o IdentitiesOnly=yes"
|
||||
}
|
||||
env = append(env, "GIT_SSH_COMMAND="+sshCommand)
|
||||
if auth.KeyPassphrase != "" {
|
||||
askPassPath, err := writeAskPassScript(auth.KeyPassphrase)
|
||||
if err != nil {
|
||||
return nil, nil, cleanup, err
|
||||
}
|
||||
cleanup = func() { _ = os.Remove(askPassPath) }
|
||||
env = append(env,
|
||||
"SSH_ASKPASS="+askPassPath,
|
||||
"SSH_ASKPASS_REQUIRE=force",
|
||||
"DISPLAY=tea",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return configs, env, cleanup, nil
|
||||
}
|
||||
|
||||
func writeAskPassScript(passphrase string) (string, error) {
|
||||
f, err := os.CreateTemp("", "tea-ssh-askpass-*")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
content := "#!/bin/sh\nprintf '%s\\n' " + shellQuote(passphrase) + "\n"
|
||||
if _, err := f.WriteString(content); err != nil {
|
||||
_ = os.Remove(f.Name())
|
||||
return "", err
|
||||
}
|
||||
if err := f.Chmod(0o700); err != nil {
|
||||
_ = os.Remove(f.Name())
|
||||
return "", err
|
||||
}
|
||||
return f.Name(), nil
|
||||
}
|
||||
|
||||
func classifyRepoError(err error) error {
|
||||
var gitErr *gitCommandError
|
||||
if errors.As(err, &gitErr) {
|
||||
msg := strings.ToLower(gitErr.stderr)
|
||||
if strings.Contains(msg, "not a git repository") || strings.Contains(msg, "cannot change to") {
|
||||
return ErrRepositoryNotExists
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func parseBranchConfigKey(key string) (branchName, field string, ok bool) {
|
||||
const prefix = "branch."
|
||||
if !strings.HasPrefix(key, prefix) {
|
||||
return "", "", false
|
||||
}
|
||||
trimmed := strings.TrimPrefix(key, prefix)
|
||||
switch {
|
||||
case strings.HasSuffix(trimmed, ".remote"):
|
||||
return strings.TrimSuffix(trimmed, ".remote"), "remote", true
|
||||
case strings.HasSuffix(trimmed, ".merge"):
|
||||
return strings.TrimSuffix(trimmed, ".merge"), "merge", true
|
||||
default:
|
||||
return "", "", false
|
||||
}
|
||||
}
|
||||
|
||||
func splitNonEmptyLines(s string) []string {
|
||||
lines := strings.Split(strings.TrimSpace(s), "\n")
|
||||
out := make([]string, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
out = append(out, line)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
// Clone clones a repository using the active backend.
|
||||
func Clone(path, remoteURL string, auth *AuthMethod, depth int, insecure bool) (*TeaRepo, error) {
|
||||
backend, err := currentBackend().Clone(path, remoteURL, auth, CloneOptions{Depth: depth, Insecure: insecure})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newTeaRepo(backend), nil
|
||||
}
|
||||
|
||||
// AddRemote adds a new remote to the repository.
|
||||
func (r TeaRepo) AddRemote(name, remoteURL string) error {
|
||||
return r.backend.AddRemote(name, remoteURL)
|
||||
}
|
||||
|
||||
// SetBranchUpstream configures the branch's upstream remote.
|
||||
func (r TeaRepo) SetBranchUpstream(branchName, remoteName, remoteBranch string) error {
|
||||
return r.backend.SetBranchUpstream(branchName, remoteName, remoteBranch)
|
||||
}
|
||||
|
||||
// Fetch fetches updates from the named remote.
|
||||
func (r TeaRepo) Fetch(remoteName string, refspecs []string, auth *AuthMethod) error {
|
||||
return r.backend.Fetch(remoteName, refspecs, auth)
|
||||
}
|
||||
+11
-18
@@ -6,14 +6,11 @@ package git
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
git_config "github.com/go-git/go-git/v5/config"
|
||||
)
|
||||
|
||||
// GetRemote tries to match a Remote of the repo via the given URL.
|
||||
// Matching is based on the normalized URL, accepting different protocols.
|
||||
func (r TeaRepo) GetRemote(remoteURL string) (*git.Remote, error) {
|
||||
func (r TeaRepo) GetRemote(remoteURL string) (*Remote, error) {
|
||||
repoURL, err := ParseURL(remoteURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -23,14 +20,14 @@ func (r TeaRepo) GetRemote(remoteURL string) (*git.Remote, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range remotes {
|
||||
for _, u := range r.Config().URLs {
|
||||
remoteURL, err := ParseURL(u)
|
||||
for _, remote := range remotes {
|
||||
for _, u := range remote.Config().URLs {
|
||||
parsedRemoteURL, err := ParseURL(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if remoteURL.Host == repoURL.Host && remoteURL.Path == repoURL.Path {
|
||||
return r, nil
|
||||
if parsedRemoteURL.Host == repoURL.Host && parsedRemoteURL.Path == repoURL.Path {
|
||||
return remote, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,27 +38,23 @@ func (r TeaRepo) GetRemote(remoteURL string) (*git.Remote, error) {
|
||||
// GetOrCreateRemote tries to match a Remote of the repo via the given URL.
|
||||
// If no match is found, a new Remote with `newRemoteName` is created.
|
||||
// Matching is based on the normalized URL, accepting different protocols.
|
||||
func (r TeaRepo) GetOrCreateRemote(remoteURL, newRemoteName string) (*git.Remote, error) {
|
||||
func (r TeaRepo) GetOrCreateRemote(remoteURL, newRemoteName string) (*Remote, error) {
|
||||
localRemote, err := r.GetRemote(remoteURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if no match found, create a new remote
|
||||
if localRemote == nil {
|
||||
localRemote, err = r.CreateRemote(&git_config.RemoteConfig{
|
||||
Name: newRemoteName,
|
||||
URLs: []string{remoteURL},
|
||||
})
|
||||
if err != nil {
|
||||
if err := r.AddRemote(newRemoteName, remoteURL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.Remote(newRemoteName)
|
||||
}
|
||||
|
||||
return localRemote, nil
|
||||
}
|
||||
|
||||
// TeaRemoteURL returns the first url entry for the given remote name
|
||||
// TeaRemoteURL returns the first url entry for the given remote name.
|
||||
func (r TeaRepo) TeaRemoteURL(name string) (auth *url.URL, err error) {
|
||||
remote, err := r.Remote(name)
|
||||
if err != nil {
|
||||
@@ -71,5 +64,5 @@ func (r TeaRepo) TeaRemoteURL(name string) (auth *url.URL, err error) {
|
||||
if len(urls) == 0 {
|
||||
return nil, fmt.Errorf("remote %s has no URL configured", name)
|
||||
}
|
||||
return ParseURL(remote.Config().URLs[0])
|
||||
return ParseURL(urls[0])
|
||||
}
|
||||
|
||||
+62
-15
@@ -4,14 +4,18 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// TeaRepo is a go-git Repository, with an extended high level interface.
|
||||
// TeaRepo wraps a local git repository behind a swappable backend.
|
||||
type TeaRepo struct {
|
||||
*git.Repository
|
||||
backend RepositoryBackend
|
||||
}
|
||||
|
||||
func newTeaRepo(backend RepositoryBackend) *TeaRepo {
|
||||
return &TeaRepo{backend: backend}
|
||||
}
|
||||
|
||||
// RepoForWorkdir tries to open the git repository in the local directory
|
||||
@@ -20,28 +24,71 @@ func RepoForWorkdir() (*TeaRepo, error) {
|
||||
return RepoFromPath("")
|
||||
}
|
||||
|
||||
// RepoFromPath tries to open the git repository by path
|
||||
// RepoFromPath tries to open the git repository by path.
|
||||
func RepoFromPath(path string) (*TeaRepo, error) {
|
||||
if len(path) == 0 {
|
||||
path = "./"
|
||||
backend, err := currentBackend().Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo, err := git.PlainOpenWithOptions(path, &git.PlainOpenOptions{
|
||||
DetectDotGit: true,
|
||||
EnableDotGitCommonDir: true, // Enable commondir support for worktrees
|
||||
})
|
||||
return newTeaRepo(backend), nil
|
||||
}
|
||||
|
||||
// WorkTree returns the repository work tree path.
|
||||
func (r TeaRepo) WorkTree() string {
|
||||
return r.backend.WorkTree()
|
||||
}
|
||||
|
||||
// Config returns the repository config values tea needs.
|
||||
func (r TeaRepo) Config() (*Config, error) {
|
||||
return r.backend.Config()
|
||||
}
|
||||
|
||||
// Remote returns the configured remote by name.
|
||||
func (r TeaRepo) Remote(remoteName string) (*Remote, error) {
|
||||
cfg, err := r.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remoteCfg, ok := cfg.Remotes[remoteName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("remote %s not found", remoteName)
|
||||
}
|
||||
return &Remote{repo: &r, config: remoteCfg}, nil
|
||||
}
|
||||
|
||||
// Remotes returns all configured remotes sorted by name.
|
||||
func (r TeaRepo) Remotes() ([]*Remote, error) {
|
||||
cfg, err := r.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TeaRepo{repo}, nil
|
||||
remoteNames := make([]string, 0, len(cfg.Remotes))
|
||||
for name := range cfg.Remotes {
|
||||
remoteNames = append(remoteNames, name)
|
||||
}
|
||||
sort.Strings(remoteNames)
|
||||
|
||||
remotes := make([]*Remote, 0, len(remoteNames))
|
||||
for _, name := range remoteNames {
|
||||
remotes = append(remotes, &Remote{repo: &r, config: cfg.Remotes[name]})
|
||||
}
|
||||
return remotes, nil
|
||||
}
|
||||
|
||||
// RemoteURL returns the URL of the given remote
|
||||
// Head returns the currently checked out ref.
|
||||
func (r TeaRepo) Head() (*Reference, error) {
|
||||
return r.backend.Head()
|
||||
}
|
||||
|
||||
// RemoteURL returns the URL of the given remote.
|
||||
func (r TeaRepo) RemoteURL(remoteName string) (*url.URL, error) {
|
||||
remote, err := r.Remote(remoteName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return url.Parse(remote.Config().URLs[0])
|
||||
if len(remote.Config().URLs) == 0 {
|
||||
return nil, fmt.Errorf("remote %s has no URL configured", remoteName)
|
||||
}
|
||||
return ParseURL(remote.Config().URLs[0])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func runGit(t *testing.T, dir string, args ...string) string {
|
||||
t.Helper()
|
||||
cmd := exec.Command("git", args...)
|
||||
if dir != "" {
|
||||
cmd.Dir = dir
|
||||
}
|
||||
out, err := cmd.CombinedOutput()
|
||||
require.NoErrorf(t, err, "git %v failed: %s", args, out)
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func tryGit(dir string, args ...string) error {
|
||||
cmd := exec.Command("git", args...)
|
||||
if dir != "" {
|
||||
cmd.Dir = dir
|
||||
}
|
||||
_, err := cmd.CombinedOutput()
|
||||
return err
|
||||
}
|
||||
|
||||
func TestRepoFromPathSupportsWorktrees(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
mainRepoPath := filepath.Join(tmpDir, "main-repo")
|
||||
worktreePath := filepath.Join(tmpDir, "worktree")
|
||||
|
||||
runGit(t, "", "init", mainRepoPath)
|
||||
runGit(t, mainRepoPath, "config", "user.email", "test@example.com")
|
||||
runGit(t, mainRepoPath, "config", "user.name", "Test User")
|
||||
runGit(t, mainRepoPath, "remote", "add", "origin", "https://gitea.com/owner/repo.git")
|
||||
|
||||
readmePath := filepath.Join(mainRepoPath, "README.md")
|
||||
require.NoError(t, os.WriteFile(readmePath, []byte("# Test Repo\n"), 0o644))
|
||||
runGit(t, mainRepoPath, "add", "README.md")
|
||||
runGit(t, mainRepoPath, "commit", "-m", "Initial commit")
|
||||
runGit(t, mainRepoPath, "worktree", "add", worktreePath, "-b", "test-branch")
|
||||
|
||||
repo, err := RepoFromPath(worktreePath)
|
||||
require.NoError(t, err)
|
||||
|
||||
config, err := repo.Config()
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, config.Remotes, "origin")
|
||||
require.Equal(t, "https://gitea.com/owner/repo.git", config.Remotes["origin"].URLs[0])
|
||||
|
||||
head, err := repo.Head()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test-branch", head.Name().Short())
|
||||
}
|
||||
|
||||
func TestRepoFromPathSupportsSHA256Repos(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
repoPath := filepath.Join(tmpDir, "sha256-repo")
|
||||
|
||||
if err := tryGit("", "init", "--object-format=sha256", repoPath); err != nil {
|
||||
t.Skip("git does not support sha256 object format in this environment")
|
||||
}
|
||||
|
||||
runGit(t, repoPath, "config", "user.email", "test@example.com")
|
||||
runGit(t, repoPath, "config", "user.name", "Test User")
|
||||
runGit(t, repoPath, "remote", "add", "origin", "https://gitea.com/owner/repo.git")
|
||||
|
||||
readmePath := filepath.Join(repoPath, "README.md")
|
||||
require.NoError(t, os.WriteFile(readmePath, []byte("sha256\n"), 0o644))
|
||||
runGit(t, repoPath, "add", "README.md")
|
||||
runGit(t, repoPath, "commit", "-m", "Initial commit")
|
||||
|
||||
repo, err := RepoFromPath(repoPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
branch, sha, err := repo.TeaGetCurrentBranchNameAndSHA()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, branch)
|
||||
require.Len(t, sha, 64)
|
||||
|
||||
config, err := repo.Config()
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, config.Remotes, "origin")
|
||||
}
|
||||
|
||||
func TestTeaFindBranchByShaAndName(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
remotePath := filepath.Join(tmpDir, "remote.git")
|
||||
localPath := filepath.Join(tmpDir, "local")
|
||||
|
||||
runGit(t, "", "init", "--bare", remotePath)
|
||||
runGit(t, "", "clone", remotePath, localPath)
|
||||
runGit(t, localPath, "config", "user.email", "test@example.com")
|
||||
runGit(t, localPath, "config", "user.name", "Test User")
|
||||
|
||||
filePath := filepath.Join(localPath, "README.md")
|
||||
require.NoError(t, os.WriteFile(filePath, []byte("main\n"), 0o644))
|
||||
runGit(t, localPath, "add", "README.md")
|
||||
runGit(t, localPath, "commit", "-m", "Initial commit")
|
||||
runGit(t, localPath, "branch", "-M", "main")
|
||||
runGit(t, localPath, "push", "-u", "origin", "main")
|
||||
runGit(t, localPath, "checkout", "-b", "feature/demo")
|
||||
|
||||
require.NoError(t, os.WriteFile(filePath, []byte("feature\n"), 0o644))
|
||||
runGit(t, localPath, "commit", "-am", "Feature commit")
|
||||
runGit(t, localPath, "push", "-u", "origin", "feature/demo")
|
||||
|
||||
repo, err := RepoFromPath(localPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
sha := strings.TrimSpace(runGit(t, localPath, "rev-parse", "HEAD"))
|
||||
branchBySha, err := repo.TeaFindBranchBySha(sha, remotePath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, branchBySha)
|
||||
require.Equal(t, "feature/demo", branchBySha.Name)
|
||||
require.Equal(t, "origin", branchBySha.Remote)
|
||||
|
||||
branchByName, err := repo.TeaFindBranchByName("feature/demo", remotePath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, branchByName)
|
||||
require.Equal(t, "feature/demo", branchByName.Name)
|
||||
|
||||
remote, err := repo.TeaFindBranchRemote("feature/demo", sha)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, remote)
|
||||
require.Equal(t, "origin", remote.Config().Name)
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrRepositoryNotExists indicates the requested path is not inside a git repository.
|
||||
ErrRepositoryNotExists = errors.New("repository does not exist")
|
||||
// ErrBranchExists indicates the requested branch already exists locally.
|
||||
ErrBranchExists = errors.New("branch already exists")
|
||||
)
|
||||
|
||||
// AuthMethod carries backend-agnostic authentication information for git operations.
|
||||
type AuthMethod struct {
|
||||
Scheme string
|
||||
Username string
|
||||
Password string
|
||||
KeyFile string
|
||||
KeyPassphrase string
|
||||
}
|
||||
|
||||
// CloneOptions describes repository clone behavior.
|
||||
type CloneOptions struct {
|
||||
Depth int
|
||||
Insecure bool
|
||||
}
|
||||
|
||||
// RepositoryBackend is the backend abstraction used by TeaRepo.
|
||||
type RepositoryBackend interface {
|
||||
WorkTree() string
|
||||
Config() (*Config, error)
|
||||
Head() (*Reference, error)
|
||||
AddRemote(name, remoteURL string) error
|
||||
SetBranchUpstream(branchName, remoteName, remoteBranch string) error
|
||||
Fetch(remoteName string, refspecs []string, auth *AuthMethod) error
|
||||
CreateTrackingBranch(localBranchName, remoteBranchName, remoteName string) error
|
||||
Checkout(ref ReferenceName) error
|
||||
DeleteLocalBranch(branchName string) error
|
||||
DeleteRemoteBranch(remoteName, remoteBranch string, auth *AuthMethod) error
|
||||
ListReferences(prefixes ...string) ([]*Reference, error)
|
||||
PushToAgitFlowPR(remoteName, head, base, topic string, pushOptions map[string]string, auth *AuthMethod) error
|
||||
}
|
||||
|
||||
// Backend opens and clones repositories using a concrete git implementation.
|
||||
type Backend interface {
|
||||
Name() string
|
||||
Open(path string) (RepositoryBackend, error)
|
||||
Clone(path, remoteURL string, auth *AuthMethod, opts CloneOptions) (RepositoryBackend, error)
|
||||
}
|
||||
|
||||
// Config mirrors the repository config fields tea needs.
|
||||
type Config struct {
|
||||
Remotes map[string]*RemoteConfig
|
||||
Branches map[string]*Branch
|
||||
}
|
||||
|
||||
// RemoteConfig stores remote configuration.
|
||||
type RemoteConfig struct {
|
||||
Name string
|
||||
URLs []string
|
||||
}
|
||||
|
||||
// Branch stores branch configuration.
|
||||
type Branch struct {
|
||||
Name string
|
||||
Remote string
|
||||
Merge ReferenceName
|
||||
}
|
||||
|
||||
// Validate checks whether the branch contains the fields tea needs.
|
||||
func (b *Branch) Validate() error {
|
||||
if b == nil || b.Name == "" {
|
||||
return errors.New("branch name is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remote wraps a configured git remote.
|
||||
type Remote struct {
|
||||
repo *TeaRepo
|
||||
config *RemoteConfig
|
||||
}
|
||||
|
||||
// Config returns the remote configuration.
|
||||
func (r *Remote) Config() *RemoteConfig {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return r.config
|
||||
}
|
||||
|
||||
// ReferenceName identifies a git reference.
|
||||
type ReferenceName string
|
||||
|
||||
func (r ReferenceName) String() string { return string(r) }
|
||||
|
||||
// Short returns the short display name for the reference.
|
||||
func (r ReferenceName) Short() string {
|
||||
s := string(r)
|
||||
switch {
|
||||
case strings.HasPrefix(s, "refs/heads/"):
|
||||
return strings.TrimPrefix(s, "refs/heads/")
|
||||
case strings.HasPrefix(s, "refs/remotes/"):
|
||||
return strings.TrimPrefix(s, "refs/remotes/")
|
||||
case strings.HasPrefix(s, "refs/tags/"):
|
||||
return strings.TrimPrefix(s, "refs/tags/")
|
||||
default:
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
// IsBranch reports whether the reference points to a local branch.
|
||||
func (r ReferenceName) IsBranch() bool {
|
||||
return strings.HasPrefix(string(r), "refs/heads/")
|
||||
}
|
||||
|
||||
// IsRemote reports whether the reference points to a remote-tracking branch.
|
||||
func (r ReferenceName) IsRemote() bool {
|
||||
return strings.HasPrefix(string(r), "refs/remotes/")
|
||||
}
|
||||
|
||||
// IsTag reports whether the reference points to a tag.
|
||||
func (r ReferenceName) IsTag() bool {
|
||||
return strings.HasPrefix(string(r), "refs/tags/")
|
||||
}
|
||||
|
||||
// Hash wraps a git object id.
|
||||
type Hash string
|
||||
|
||||
func (h Hash) String() string { return string(h) }
|
||||
|
||||
// Reference stores a resolved git ref and its hash.
|
||||
type Reference struct {
|
||||
name ReferenceName
|
||||
hash Hash
|
||||
}
|
||||
|
||||
// Name returns the reference name.
|
||||
func (r *Reference) Name() ReferenceName { return r.name }
|
||||
|
||||
// Hash returns the reference hash.
|
||||
func (r *Reference) Hash() Hash { return r.hash }
|
||||
|
||||
// NewBranchReferenceName constructs a local branch ref name.
|
||||
func NewBranchReferenceName(name string) ReferenceName {
|
||||
if strings.HasPrefix(name, "refs/") {
|
||||
return ReferenceName(name)
|
||||
}
|
||||
return ReferenceName("refs/heads/" + name)
|
||||
}
|
||||
|
||||
// NewRemoteReferenceName constructs a remote-tracking ref name.
|
||||
func NewRemoteReferenceName(remote, name string) ReferenceName {
|
||||
if strings.HasPrefix(name, "refs/") {
|
||||
return ReferenceName(name)
|
||||
}
|
||||
return ReferenceName("refs/remotes/" + remote + "/" + name)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"code.gitea.io/tea/modules/version"
|
||||
"gitea.dev/tea/modules/version"
|
||||
)
|
||||
|
||||
// UserAgent returns the standard User-Agent string for tea.
|
||||
|
||||
@@ -4,14 +4,16 @@
|
||||
package interact
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
"code.gitea.io/tea/modules/print"
|
||||
"code.gitea.io/tea/modules/theme"
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"gitea.dev/tea/cmd/flags"
|
||||
"gitea.dev/tea/modules/context"
|
||||
"gitea.dev/tea/modules/print"
|
||||
"gitea.dev/tea/modules/theme"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
"golang.org/x/term"
|
||||
@@ -19,18 +21,18 @@ import (
|
||||
|
||||
// ShowCommentsMaybeInteractive fetches & prints comments, depending on the --comments flag.
|
||||
// If that flag is unset, and output is not piped, prompts the user first.
|
||||
func ShowCommentsMaybeInteractive(ctx *context.TeaContext, idx int64, totalComments int) error {
|
||||
func ShowCommentsMaybeInteractive(requestCtx stdctx.Context, ctx *context.TeaContext, idx int64, totalComments int) error {
|
||||
if ctx.Bool("comments") {
|
||||
opts := gitea.ListIssueCommentOptions{ListOptions: flags.GetListOptions(ctx.Command)}
|
||||
c := ctx.Login.Client()
|
||||
comments, _, err := c.ListIssueComments(ctx.Owner, ctx.Repo, idx, opts)
|
||||
comments, _, err := c.Issues.ListIssueComments(requestCtx, ctx.Owner, ctx.Repo, idx, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
print.Comments(comments)
|
||||
} else if print.IsInteractive() && !ctx.IsSet("comments") {
|
||||
// if we're interactive, but --comments hasn't been explicitly set to false
|
||||
if err := ShowCommentsPaginated(ctx, idx, totalComments); err != nil {
|
||||
if err := ShowCommentsPaginated(requestCtx, ctx, idx, totalComments); err != nil {
|
||||
fmt.Printf("error while loading comments: %v\n", err)
|
||||
}
|
||||
}
|
||||
@@ -38,7 +40,7 @@ func ShowCommentsMaybeInteractive(ctx *context.TeaContext, idx int64, totalComme
|
||||
}
|
||||
|
||||
// ShowCommentsPaginated prompts if issue/pr comments should be shown and continues to do so.
|
||||
func ShowCommentsPaginated(ctx *context.TeaContext, idx int64, totalComments int) error {
|
||||
func ShowCommentsPaginated(requestCtx stdctx.Context, ctx *context.TeaContext, idx int64, totalComments int) error {
|
||||
c := ctx.Login.Client()
|
||||
opts := gitea.ListIssueCommentOptions{ListOptions: flags.GetListOptions(ctx.Command)}
|
||||
prompt := "show comments?"
|
||||
@@ -58,7 +60,7 @@ func ShowCommentsPaginated(ctx *context.TeaContext, idx int64, totalComments int
|
||||
} else if !loadComments {
|
||||
break
|
||||
} else {
|
||||
if comments, _, err := c.ListIssueComments(ctx.Owner, ctx.Repo, idx, opts); err != nil {
|
||||
if comments, _, err := c.Issues.ListIssueComments(requestCtx, ctx.Owner, ctx.Repo, idx, opts); err != nil {
|
||||
return err
|
||||
} else if len(comments) != 0 {
|
||||
print.Comments(comments)
|
||||
|
||||
@@ -4,12 +4,14 @@
|
||||
package interact
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"code.gitea.io/tea/modules/task"
|
||||
"code.gitea.io/tea/modules/theme"
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"gitea.dev/tea/modules/config"
|
||||
"gitea.dev/tea/modules/task"
|
||||
"gitea.dev/tea/modules/theme"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
)
|
||||
@@ -20,7 +22,7 @@ func IsQuitting(err error) bool {
|
||||
}
|
||||
|
||||
// CreateIssue interactively creates an issue
|
||||
func CreateIssue(login *config.Login, owner, repo string) error {
|
||||
func CreateIssue(ctx context.Context, login *config.Login, owner, repo string) error {
|
||||
owner, repo, err := promptRepoSlug(owner, repo)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -28,19 +30,19 @@ func CreateIssue(login *config.Login, owner, repo string) error {
|
||||
printTitleAndContent("Target repo:", owner+"/"+repo)
|
||||
|
||||
var opts gitea.CreateIssueOption
|
||||
if err := promptIssueProperties(login, owner, repo, &opts); err != nil {
|
||||
if err := promptIssueProperties(ctx, login, owner, repo, &opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return task.CreateIssue(login, owner, repo, opts)
|
||||
return task.CreateIssue(ctx, login, owner, repo, opts)
|
||||
}
|
||||
|
||||
func promptIssueProperties(login *config.Login, owner, repo string, o *gitea.CreateIssueOption) error {
|
||||
func promptIssueProperties(ctx context.Context, login *config.Login, owner, repo string, o *gitea.CreateIssueOption) error {
|
||||
var milestoneName string
|
||||
var err error
|
||||
|
||||
selectableChan := make(chan (issueSelectables), 1)
|
||||
go fetchIssueSelectables(login, owner, repo, selectableChan)
|
||||
go fetchIssueSelectables(ctx, login, owner, repo, selectableChan)
|
||||
|
||||
// title
|
||||
if err := huh.NewInput().
|
||||
@@ -139,12 +141,12 @@ type issueSelectables struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func fetchIssueSelectables(login *config.Login, owner, repo string, done chan issueSelectables) {
|
||||
func fetchIssueSelectables(ctx context.Context, login *config.Login, owner, repo string, done chan issueSelectables) {
|
||||
// TODO PERF make these calls concurrent
|
||||
r := issueSelectables{}
|
||||
c := login.Client()
|
||||
|
||||
r.Repo, _, r.Err = c.GetRepo(owner, repo)
|
||||
r.Repo, _, r.Err = c.Repositories.GetRepo(ctx, owner, repo)
|
||||
if r.Err != nil {
|
||||
done <- r
|
||||
return
|
||||
@@ -156,7 +158,7 @@ func fetchIssueSelectables(login *config.Login, owner, repo string, done chan is
|
||||
return
|
||||
}
|
||||
|
||||
assignees, _, err := c.GetAssignees(owner, repo)
|
||||
assignees, _, err := c.Repositories.GetAssignees(ctx, owner, repo)
|
||||
if err != nil {
|
||||
r.Err = err
|
||||
done <- r
|
||||
@@ -167,7 +169,7 @@ func fetchIssueSelectables(login *config.Login, owner, repo string, done chan is
|
||||
r.Assignees[i] = u.UserName
|
||||
}
|
||||
|
||||
milestones, _, err := c.ListRepoMilestones(owner, repo, gitea.ListMilestoneOption{})
|
||||
milestones, _, err := c.Repositories.ListMilestones(ctx, owner, repo, gitea.ListMilestoneOption{})
|
||||
if err != nil {
|
||||
r.Err = err
|
||||
done <- r
|
||||
@@ -183,7 +185,7 @@ func fetchIssueSelectables(login *config.Login, owner, repo string, done chan is
|
||||
r.LabelMap = make(map[string]int64)
|
||||
r.LabelList = make([]string, 0)
|
||||
for page := 1; ; {
|
||||
labels, resp, err := c.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{
|
||||
labels, resp, err := c.Repositories.ListRepoLabels(ctx, owner, repo, gitea.ListLabelsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -4,19 +4,20 @@
|
||||
package interact
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
"code.gitea.io/tea/modules/task"
|
||||
"code.gitea.io/tea/modules/theme"
|
||||
"gitea.dev/tea/modules/config"
|
||||
"gitea.dev/tea/modules/context"
|
||||
"gitea.dev/tea/modules/task"
|
||||
"gitea.dev/tea/modules/theme"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
)
|
||||
|
||||
// EditIssue interactively edits an issue
|
||||
func EditIssue(ctx context.TeaContext, index int64) (*task.EditIssueOption, error) {
|
||||
func EditIssue(requestCtx stdctx.Context, ctx context.TeaContext, index int64) (*task.EditIssueOption, error) {
|
||||
opts := task.EditIssueOption{}
|
||||
var err error
|
||||
|
||||
@@ -27,7 +28,7 @@ func EditIssue(ctx context.TeaContext, index int64) (*task.EditIssueOption, erro
|
||||
printTitleAndContent("Target repo:", ctx.Owner+"/"+ctx.Repo)
|
||||
|
||||
c := ctx.Login.Client()
|
||||
i, _, err := c.GetIssue(ctx.Owner, ctx.Repo, index)
|
||||
i, _, err := c.Issues.GetIssue(requestCtx, ctx.Owner, ctx.Repo, index)
|
||||
if err != nil {
|
||||
return &opts, err
|
||||
}
|
||||
@@ -55,20 +56,20 @@ func EditIssue(ctx context.TeaContext, index int64) (*task.EditIssueOption, erro
|
||||
opts.Milestone = &i.Milestone.Title
|
||||
}
|
||||
|
||||
if err := promptIssueEditProperties(&ctx, &opts); err != nil {
|
||||
if err := promptIssueEditProperties(requestCtx, &ctx, &opts); err != nil {
|
||||
return &opts, err
|
||||
}
|
||||
|
||||
return &opts, err
|
||||
}
|
||||
|
||||
func promptIssueEditProperties(ctx *context.TeaContext, o *task.EditIssueOption) error {
|
||||
func promptIssueEditProperties(requestCtx stdctx.Context, ctx *context.TeaContext, o *task.EditIssueOption) error {
|
||||
var milestoneName string
|
||||
var labelsSelected []string
|
||||
var err error
|
||||
|
||||
selectableChan := make(chan (issueSelectables), 1)
|
||||
go fetchIssueSelectables(ctx.Login, ctx.Owner, ctx.Repo, selectableChan)
|
||||
go fetchIssueSelectables(requestCtx, ctx.Login, ctx.Owner, ctx.Repo, selectableChan)
|
||||
|
||||
// title
|
||||
if err := huh.NewInput().
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package interact
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
@@ -11,17 +12,18 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/modules/auth"
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"code.gitea.io/tea/modules/task"
|
||||
"code.gitea.io/tea/modules/theme"
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"gitea.dev/tea/modules/auth"
|
||||
"gitea.dev/tea/modules/config"
|
||||
"gitea.dev/tea/modules/task"
|
||||
"gitea.dev/tea/modules/theme"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
)
|
||||
|
||||
// CreateLogin create an login interactive
|
||||
func CreateLogin() error {
|
||||
func CreateLogin(ctx context.Context) error {
|
||||
var (
|
||||
name, token, user, passwd, otp, scopes, sshKey, sshCertPrincipal, sshKeyFingerprint string
|
||||
insecure, sshAgent, versionCheck, helper bool
|
||||
@@ -103,7 +105,7 @@ func CreateLogin() error {
|
||||
}
|
||||
printTitleAndContent("Allow Insecure connections:", strconv.FormatBool(insecure))
|
||||
|
||||
return auth.OAuthLoginWithOptions(name, giteaURL, insecure)
|
||||
return auth.OAuthLoginWithOptions(ctx, name, giteaURL, insecure)
|
||||
default: // token
|
||||
var hasToken bool
|
||||
if err := huh.NewConfirm().
|
||||
@@ -269,7 +271,7 @@ func CreateLogin() error {
|
||||
printTitleAndContent("Check version of Gitea instance:", strconv.FormatBool(versionCheck))
|
||||
}
|
||||
|
||||
return task.CreateLogin(name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCertPrincipal, sshKeyFingerprint, insecure, sshAgent, versionCheck, helper)
|
||||
return task.CreateLogin(ctx, name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCertPrincipal, sshKeyFingerprint, insecure, sshAgent, versionCheck, helper)
|
||||
}
|
||||
|
||||
var tokenScopeOpts = []string{
|
||||
|
||||
@@ -4,19 +4,20 @@
|
||||
package interact
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"code.gitea.io/tea/modules/task"
|
||||
"code.gitea.io/tea/modules/theme"
|
||||
gitea "gitea.dev/sdk"
|
||||
"gitea.dev/tea/modules/config"
|
||||
"gitea.dev/tea/modules/task"
|
||||
"gitea.dev/tea/modules/theme"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
)
|
||||
|
||||
// CreateMilestone interactively creates a milestone
|
||||
func CreateMilestone(login *config.Login, owner, repo string) error {
|
||||
func CreateMilestone(requestCtx stdctx.Context, login *config.Login, owner, repo string) error {
|
||||
var title, description, deadline string
|
||||
|
||||
// owner, repo
|
||||
@@ -59,8 +60,7 @@ func CreateMilestone(login *config.Login, owner, repo string) error {
|
||||
deadlineTM = &tm
|
||||
}
|
||||
|
||||
return task.CreateMilestone(
|
||||
login,
|
||||
return task.CreateMilestone(requestCtx, login,
|
||||
owner,
|
||||
repo,
|
||||
title,
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"code.gitea.io/tea/modules/theme"
|
||||
"gitea.dev/tea/modules/theme"
|
||||
|
||||
"charm.land/lipgloss/v2"
|
||||
)
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/tea/modules/theme"
|
||||
"code.gitea.io/tea/modules/utils"
|
||||
"gitea.dev/tea/modules/theme"
|
||||
"gitea.dev/tea/modules/utils"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
)
|
||||
|
||||
@@ -4,16 +4,18 @@
|
||||
package interact
|
||||
|
||||
import (
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
"code.gitea.io/tea/modules/task"
|
||||
"code.gitea.io/tea/modules/theme"
|
||||
stdctx "context"
|
||||
|
||||
gitea "gitea.dev/sdk"
|
||||
"gitea.dev/tea/modules/context"
|
||||
"gitea.dev/tea/modules/task"
|
||||
"gitea.dev/tea/modules/theme"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
)
|
||||
|
||||
// CreatePull interactively creates a PR
|
||||
func CreatePull(ctx *context.TeaContext) (err error) {
|
||||
func CreatePull(requestCtx stdctx.Context, ctx *context.TeaContext) (err error) {
|
||||
var (
|
||||
base, head string
|
||||
allowMaintainerEdits = true
|
||||
@@ -27,7 +29,7 @@ func CreatePull(ctx *context.TeaContext) (err error) {
|
||||
}
|
||||
|
||||
// base
|
||||
if base, err = task.GetDefaultPRBase(ctx.Login, ctx.Owner, ctx.Repo); err != nil {
|
||||
if base, err = task.GetDefaultPRBase(requestCtx, ctx.Login, ctx.Owner, ctx.Repo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -85,11 +87,12 @@ func CreatePull(ctx *context.TeaContext) (err error) {
|
||||
}
|
||||
|
||||
opts := gitea.CreateIssueOption{Title: task.GetDefaultPRTitle(head)}
|
||||
if err = promptIssueProperties(ctx.Login, ctx.Owner, ctx.Repo, &opts); err != nil {
|
||||
if err = promptIssueProperties(requestCtx, ctx.Login, ctx.Owner, ctx.Repo, &opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return task.CreateAgitFlowPull(
|
||||
requestCtx,
|
||||
ctx,
|
||||
baseRemote,
|
||||
head,
|
||||
@@ -127,11 +130,12 @@ func CreatePull(ctx *context.TeaContext) (err error) {
|
||||
head = task.GetHeadSpec(headOwner, headBranch, ctx.Owner)
|
||||
|
||||
opts := gitea.CreateIssueOption{Title: task.GetDefaultPRTitle(head)}
|
||||
if err = promptIssueProperties(ctx.Login, ctx.Owner, ctx.Repo, &opts); err != nil {
|
||||
if err = promptIssueProperties(requestCtx, ctx.Login, ctx.Owner, ctx.Repo, &opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return task.CreatePull(
|
||||
requestCtx,
|
||||
ctx,
|
||||
base,
|
||||
head,
|
||||
|
||||
@@ -4,20 +4,22 @@
|
||||
package interact
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
"code.gitea.io/tea/modules/task"
|
||||
"code.gitea.io/tea/modules/utils"
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"gitea.dev/tea/cmd/flags"
|
||||
"gitea.dev/tea/modules/context"
|
||||
"gitea.dev/tea/modules/task"
|
||||
"gitea.dev/tea/modules/utils"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
)
|
||||
|
||||
// MergePull interactively creates a PR
|
||||
func MergePull(ctx *context.TeaContext) error {
|
||||
func MergePull(requestCtx stdctx.Context, ctx *context.TeaContext) error {
|
||||
if ctx.LocalRepo == nil {
|
||||
return fmt.Errorf("pull request index is required")
|
||||
}
|
||||
@@ -27,12 +29,12 @@ func MergePull(ctx *context.TeaContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
idx, err := getPullIndex(ctx, branch)
|
||||
idx, err := getPullIndex(requestCtx, ctx, branch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return task.PullMerge(ctx.Login, ctx.Owner, ctx.Repo, idx, gitea.MergePullRequestOption{
|
||||
return task.PullMerge(requestCtx, ctx.Login, ctx.Owner, ctx.Repo, idx, gitea.MergePullRequestOption{
|
||||
Style: gitea.MergeStyle(ctx.String("style")),
|
||||
Title: ctx.String("title"),
|
||||
Message: ctx.String("message"),
|
||||
@@ -40,7 +42,7 @@ func MergePull(ctx *context.TeaContext) error {
|
||||
}
|
||||
|
||||
// getPullIndex interactively determines the PR index
|
||||
func getPullIndex(ctx *context.TeaContext, branch string) (int64, error) {
|
||||
func getPullIndex(requestCtx stdctx.Context, ctx *context.TeaContext, branch string) (int64, error) {
|
||||
c := ctx.Login.Client()
|
||||
opts := gitea.ListPullRequestsOptions{
|
||||
State: gitea.StateOpen,
|
||||
@@ -53,7 +55,7 @@ func getPullIndex(ctx *context.TeaContext, branch string) (int64, error) {
|
||||
var prs []*gitea.PullRequest
|
||||
for {
|
||||
var err error
|
||||
prs, _, err = c.ListRepoPullRequests(ctx.Owner, ctx.Repo, opts)
|
||||
prs, _, err = c.PullRequests.ListRepoPullRequests(requestCtx, ctx.Owner, ctx.Repo, opts)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -4,15 +4,17 @@
|
||||
package interact
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
"code.gitea.io/tea/modules/task"
|
||||
"code.gitea.io/tea/modules/theme"
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"gitea.dev/tea/modules/config"
|
||||
"gitea.dev/tea/modules/context"
|
||||
"gitea.dev/tea/modules/task"
|
||||
"gitea.dev/tea/modules/theme"
|
||||
|
||||
"charm.land/huh/v2"
|
||||
)
|
||||
@@ -25,7 +27,7 @@ var reviewStates = map[string]gitea.ReviewStateType{
|
||||
var reviewStateOptions = []string{"comment", "request changes", "approve"}
|
||||
|
||||
// ReviewPull interactively reviews a PR
|
||||
func ReviewPull(ctx *context.TeaContext, idx int64) error {
|
||||
func ReviewPull(requestCtx stdctx.Context, ctx *context.TeaContext, idx int64) error {
|
||||
var state gitea.ReviewStateType
|
||||
var comment string
|
||||
var codeComments []gitea.CreatePullReviewComment
|
||||
@@ -43,7 +45,7 @@ func ReviewPull(ctx *context.TeaContext, idx int64) error {
|
||||
printTitleAndContent("Review / comment the diff?", strconv.FormatBool(reviewDiff))
|
||||
|
||||
if reviewDiff {
|
||||
if codeComments, err = DoDiffReview(ctx, idx); err != nil {
|
||||
if codeComments, err = DoDiffReview(requestCtx, ctx, idx); err != nil {
|
||||
fmt.Printf("Error during diff review: %s\n", err)
|
||||
}
|
||||
fmt.Printf("Found %d code comments in your review\n", len(codeComments))
|
||||
@@ -77,14 +79,14 @@ func ReviewPull(ctx *context.TeaContext, idx int64) error {
|
||||
}
|
||||
printTitleAndContent("Concluding comment(markdown):", comment)
|
||||
|
||||
return task.CreatePullReview(ctx, idx, state, comment, codeComments)
|
||||
return task.CreatePullReview(requestCtx, ctx, idx, state, comment, codeComments)
|
||||
}
|
||||
|
||||
// DoDiffReview (1) fetches & saves diff in tempfile, (2) starts $VISUAL or $EDITOR to comment on diff,
|
||||
// (3) parses resulting file into code comments.
|
||||
// It doesn't really make sense to use survey.Editor() here, as we'd read the file content at least twice.
|
||||
func DoDiffReview(ctx *context.TeaContext, idx int64) ([]gitea.CreatePullReviewComment, error) {
|
||||
tmpFile, err := task.SavePullDiff(ctx, idx)
|
||||
func DoDiffReview(requestCtx stdctx.Context, ctx *context.TeaContext, idx int64) ([]gitea.CreatePullReviewComment, error) {
|
||||
tmpFile, err := task.SavePullDiff(requestCtx, ctx, idx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ package print
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// ActionSecretsList prints a list of action secrets
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// formatDurationMinutes formats duration in a human-readable way
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ package print
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
func formatByteSize(size int64) string {
|
||||
|
||||
@@ -6,7 +6,7 @@ package print
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// BranchesList prints a listing of the branches
|
||||
|
||||
@@ -6,7 +6,7 @@ package print
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// Comments renders a list of comments to stdout
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
"github.com/muesli/termenv"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
"github.com/enescakir/emoji"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ package print
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// LabelsList prints a listing of labels
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"gitea.dev/tea/modules/config"
|
||||
)
|
||||
|
||||
// LoginDetails print login entry to stdout
|
||||
|
||||
@@ -6,7 +6,7 @@ package print
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// MilestoneDetails print an milestone formatted to stdout
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// NotificationsList prints a listing of notification threads
|
||||
|
||||
@@ -6,7 +6,7 @@ package print
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// OrganizationDetails prints details of an org with formatting
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
var ciStatusSymbols = map[gitea.StatusState]string{
|
||||
|
||||
@@ -6,7 +6,7 @@ package print
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// PullReviewCommentFields are all available fields to print with PullReviewCommentsList()
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
package print
|
||||
|
||||
import (
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// ReleasesList prints a listing of releases
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// ReposList prints a listing of the repos
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ package print
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// SSHKeysList prints a table of SSH public keys
|
||||
|
||||
@@ -6,7 +6,7 @@ package print
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// TrackedTimesList print list of tracked times to stdout
|
||||
|
||||
@@ -6,7 +6,7 @@ package print
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// UserDetails print a formatted user to stdout
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// WebhooksList prints a listing of webhooks
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,21 +4,23 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"code.gitea.io/tea/modules/print"
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"gitea.dev/tea/modules/config"
|
||||
"gitea.dev/tea/modules/print"
|
||||
)
|
||||
|
||||
// CreateIssue creates an issue in the given repo and prints the result
|
||||
func CreateIssue(login *config.Login, repoOwner, repoName string, opts gitea.CreateIssueOption) error {
|
||||
func CreateIssue(requestCtx stdctx.Context, rlogin *config.Login, repoOwner, repoName string, opts gitea.CreateIssueOption) error {
|
||||
// title is required
|
||||
if len(opts.Title) == 0 {
|
||||
return fmt.Errorf("title is required")
|
||||
}
|
||||
|
||||
issue, _, err := login.Client().CreateIssue(repoOwner, repoName, opts)
|
||||
issue, _, err := rlogin.Client().Issues.CreateIssue(requestCtx, repoOwner, repoName, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create issue: %s", err)
|
||||
}
|
||||
|
||||
+13
-11
@@ -4,11 +4,13 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"gitea.dev/tea/modules/context"
|
||||
)
|
||||
|
||||
// EditIssueOption wraps around gitea.EditIssueOption which has bad & incosistent semantics.
|
||||
@@ -29,12 +31,12 @@ type EditIssueOption struct {
|
||||
|
||||
// Normalizes the options into parameters that can be passed to the sdk.
|
||||
// the returned value will be nil, when no change to this part of the issue is requested.
|
||||
func (o EditIssueOption) toSdkOptions(ctx *context.TeaContext, client *gitea.Client) (*gitea.EditIssueOption, *gitea.IssueLabelsOption, *gitea.IssueLabelsOption, error) {
|
||||
addLabelOpts, err := ResolveLabelOpts(client, ctx.Owner, ctx.Repo, o.AddLabels)
|
||||
func (o EditIssueOption) toSdkOptions(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea.Client) (*gitea.EditIssueOption, *gitea.IssueLabelsOption, *gitea.IssueLabelsOption, error) {
|
||||
addLabelOpts, err := ResolveLabelOpts(requestCtx, client, ctx.Owner, ctx.Repo, o.AddLabels)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
rmLabelOpts, err := ResolveLabelOpts(client, ctx.Owner, ctx.Repo, o.RemoveLabels)
|
||||
rmLabelOpts, err := ResolveLabelOpts(requestCtx, client, ctx.Owner, ctx.Repo, o.RemoveLabels)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
@@ -54,7 +56,7 @@ func (o EditIssueOption) toSdkOptions(ctx *context.TeaContext, client *gitea.Cli
|
||||
issueOptsDirty = true
|
||||
}
|
||||
if o.Milestone != nil {
|
||||
id, err := ResolveMilestoneID(client, ctx.Owner, ctx.Repo, *o.Milestone)
|
||||
id, err := ResolveMilestoneID(requestCtx, client, ctx.Owner, ctx.Repo, *o.Milestone)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
@@ -80,28 +82,28 @@ func (o EditIssueOption) toSdkOptions(ctx *context.TeaContext, client *gitea.Cli
|
||||
}
|
||||
|
||||
// EditIssue edits an issue and returns the updated issue.
|
||||
func EditIssue(ctx *context.TeaContext, client *gitea.Client, opts EditIssueOption) (*gitea.Issue, error) {
|
||||
func EditIssue(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea.Client, opts EditIssueOption) (*gitea.Issue, error) {
|
||||
if client == nil {
|
||||
client = ctx.Login.Client()
|
||||
}
|
||||
|
||||
issueOpts, addLabelOpts, rmLabelOpts, err := opts.toSdkOptions(ctx, client)
|
||||
issueOpts, addLabelOpts, rmLabelOpts, err := opts.toSdkOptions(requestCtx, ctx, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := ApplyLabelChanges(client, ctx.Owner, ctx.Repo, opts.Index, addLabelOpts, rmLabelOpts); err != nil {
|
||||
if err := ApplyLabelChanges(requestCtx, client, ctx.Owner, ctx.Repo, opts.Index, addLabelOpts, rmLabelOpts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var issue *gitea.Issue
|
||||
if issueOpts != nil {
|
||||
issue, _, err = client.EditIssue(ctx.Owner, ctx.Repo, opts.Index, *issueOpts)
|
||||
issue, _, err = client.Issues.EditIssue(requestCtx, ctx.Owner, ctx.Repo, opts.Index, *issueOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not edit issue: %s", err)
|
||||
}
|
||||
} else {
|
||||
issue, _, err = client.GetIssue(ctx.Owner, ctx.Repo, opts.Index)
|
||||
issue, _, err = client.Issues.GetIssue(requestCtx, ctx.Owner, ctx.Repo, opts.Index)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get issue: %s", err)
|
||||
}
|
||||
|
||||
+16
-14
@@ -4,18 +4,20 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/modules/utils"
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"gitea.dev/tea/modules/utils"
|
||||
)
|
||||
|
||||
// ResolveLabelNames returns a list of label IDs for a given list of label names
|
||||
func ResolveLabelNames(client *gitea.Client, owner, repo string, labelNames []string) ([]int64, error) {
|
||||
func ResolveLabelNames(requestCtx stdctx.Context, client *gitea.Client, owner, repo string, labelNames []string) ([]int64, error) {
|
||||
labelIDs := make([]int64, 0, len(labelNames))
|
||||
page := 1
|
||||
for {
|
||||
labels, resp, err := client.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{
|
||||
labels, resp, err := client.Repositories.ListRepoLabels(requestCtx, owner, repo, gitea.ListLabelsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -35,11 +37,11 @@ func ResolveLabelNames(client *gitea.Client, owner, repo string, labelNames []st
|
||||
}
|
||||
|
||||
// ResolveLabelOpts resolves label names to IssueLabelsOption. Returns nil if names is empty.
|
||||
func ResolveLabelOpts(client *gitea.Client, owner, repo string, names []string) (*gitea.IssueLabelsOption, error) {
|
||||
func ResolveLabelOpts(requestCtx stdctx.Context, client *gitea.Client, owner, repo string, names []string) (*gitea.IssueLabelsOption, error) {
|
||||
if len(names) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
ids, err := ResolveLabelNames(client, owner, repo, names)
|
||||
ids, err := ResolveLabelNames(requestCtx, client, owner, repo, names)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -47,18 +49,18 @@ func ResolveLabelOpts(client *gitea.Client, owner, repo string, names []string)
|
||||
}
|
||||
|
||||
// ApplyLabelChanges adds and removes labels on an issue or pull request.
|
||||
func ApplyLabelChanges(client *gitea.Client, owner, repo string, index int64, add, rm *gitea.IssueLabelsOption) error {
|
||||
func ApplyLabelChanges(requestCtx stdctx.Context, client *gitea.Client, owner, repo string, index int64, add, rm *gitea.IssueLabelsOption) error {
|
||||
if rm != nil {
|
||||
// NOTE: as of 1.17, there is no API to remove multiple labels at once.
|
||||
for _, id := range rm.Labels {
|
||||
_, err := client.DeleteIssueLabel(owner, repo, index, id)
|
||||
_, err := client.Issues.DeleteIssueLabel(requestCtx, owner, repo, index, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not remove labels: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if add != nil {
|
||||
_, _, err := client.AddIssueLabels(owner, repo, index, *add)
|
||||
_, _, err := client.Issues.AddIssueLabels(requestCtx, owner, repo, index, *add)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not add labels: %s", err)
|
||||
}
|
||||
@@ -67,9 +69,9 @@ func ApplyLabelChanges(client *gitea.Client, owner, repo string, index int64, ad
|
||||
}
|
||||
|
||||
// ApplyReviewerChanges adds and removes reviewers on a pull request.
|
||||
func ApplyReviewerChanges(client *gitea.Client, owner, repo string, index int64, add, rm []string) error {
|
||||
func ApplyReviewerChanges(requestCtx stdctx.Context, client *gitea.Client, owner, repo string, index int64, add, rm []string) error {
|
||||
if len(rm) != 0 {
|
||||
_, err := client.DeleteReviewRequests(owner, repo, index, gitea.PullReviewRequestOptions{
|
||||
_, err := client.PullRequests.DeleteReviewRequests(requestCtx, owner, repo, index, gitea.PullReviewRequestOptions{
|
||||
Reviewers: rm,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -77,7 +79,7 @@ func ApplyReviewerChanges(client *gitea.Client, owner, repo string, index int64,
|
||||
}
|
||||
}
|
||||
if len(add) != 0 {
|
||||
_, err := client.CreateReviewRequests(owner, repo, index, gitea.PullReviewRequestOptions{
|
||||
_, err := client.PullRequests.CreateReviewRequests(requestCtx, owner, repo, index, gitea.PullReviewRequestOptions{
|
||||
Reviewers: add,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -88,11 +90,11 @@ func ApplyReviewerChanges(client *gitea.Client, owner, repo string, index int64,
|
||||
}
|
||||
|
||||
// ResolveMilestoneID resolves a milestone name to its ID. Returns 0 for empty name.
|
||||
func ResolveMilestoneID(client *gitea.Client, owner, repo, name string) (int64, error) {
|
||||
func ResolveMilestoneID(requestCtx stdctx.Context, client *gitea.Client, owner, repo, name string) (int64, error) {
|
||||
if name == "" {
|
||||
return 0, nil
|
||||
}
|
||||
ms, _, err := client.GetMilestoneByName(owner, repo, name)
|
||||
ms, _, err := client.Repositories.GetMilestoneByName(requestCtx, owner, repo, name)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("could not resolve milestone '%s': %w", name, err)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// LabelsExport save list of labels to disc
|
||||
|
||||
@@ -4,16 +4,17 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"code.gitea.io/tea/modules/utils"
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/tea/modules/config"
|
||||
"gitea.dev/tea/modules/utils"
|
||||
)
|
||||
|
||||
// SetupHelper add tea helper to config global
|
||||
@@ -48,7 +49,7 @@ func SetupHelper(login config.Login) (ok bool, err error) {
|
||||
}
|
||||
|
||||
// CreateLogin create a login to be stored in config
|
||||
func CreateLogin(name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCertPrincipal, sshKeyFingerprint string, insecure, sshAgent, versionCheck, addHelper bool) error {
|
||||
func CreateLogin(ctx stdctx.Context, name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCertPrincipal, sshKeyFingerprint string, insecure, sshAgent, versionCheck, addHelper bool) error {
|
||||
// checks ...
|
||||
// ... if we have a url
|
||||
if len(giteaURL) == 0 {
|
||||
@@ -105,7 +106,7 @@ func CreateLogin(name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCe
|
||||
}
|
||||
|
||||
if len(token) == 0 && sshCertPrincipal == "" && !sshAgent && sshKey == "" {
|
||||
if login.Token, err = generateToken(login, user, passwd, otp, scopes); err != nil {
|
||||
if login.Token, err = generateToken(ctx, login, user, passwd, otp, scopes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -113,7 +114,7 @@ func CreateLogin(name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCe
|
||||
client := login.Client()
|
||||
|
||||
// Verify if authentication works and get user info
|
||||
u, _, err := client.GetMyUserInfo()
|
||||
u, _, err := client.Users.GetMyUserInfo(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -130,7 +131,7 @@ func CreateLogin(name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCe
|
||||
login.SSHHost = serverURL.Host
|
||||
|
||||
if len(sshKey) == 0 {
|
||||
login.SSHKey, err = findSSHKey(client)
|
||||
login.SSHKey, err = findSSHKey(ctx, client)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: problem while finding a SSH key: %s\n", err)
|
||||
}
|
||||
@@ -159,7 +160,7 @@ func shouldCheckTokenUniqueness(token string, sshAgent bool, sshKey, sshCertPrin
|
||||
}
|
||||
|
||||
// generateToken creates a new token when given BasicAuth credentials
|
||||
func generateToken(login config.Login, user, pass, otp, scopes string) (string, error) {
|
||||
func generateToken(ctx stdctx.Context, login config.Login, user, pass, otp, scopes string) (string, error) {
|
||||
opts := []gitea.ClientOption{gitea.SetBasicAuth(user, pass)}
|
||||
if otp != "" {
|
||||
opts = append(opts, gitea.SetOTP(otp))
|
||||
@@ -168,7 +169,7 @@ func generateToken(login config.Login, user, pass, otp, scopes string) (string,
|
||||
|
||||
var tl []*gitea.AccessToken
|
||||
for page := 1; ; {
|
||||
page_tokens, resp, err := client.ListAccessTokens(gitea.ListAccessTokensOptions{
|
||||
page_tokens, resp, err := client.Users.ListAccessTokens(ctx, gitea.ListAccessTokensOptions{
|
||||
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -200,7 +201,7 @@ func generateToken(login config.Login, user, pass, otp, scopes string) (string,
|
||||
}
|
||||
}
|
||||
|
||||
t, _, err := client.CreateAccessToken(gitea.CreateAccessTokenOption{
|
||||
t, _, err := client.Users.CreateAccessToken(ctx, gitea.CreateAccessTokenOption{
|
||||
Name: tokenName,
|
||||
Scopes: tokenScopes,
|
||||
})
|
||||
|
||||
@@ -8,9 +8,10 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/modules/utils"
|
||||
"gitea.dev/sdk"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"gitea.dev/tea/modules/utils"
|
||||
)
|
||||
|
||||
// ListSSHPubkey lists all the ssh keys in the ssh agent and the ~/.ssh/*.pub files
|
||||
|
||||
@@ -4,24 +4,25 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/tea/modules/utils"
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/tea/modules/utils"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// findSSHKey retrieves the ssh keys registered in gitea, and tries to find
|
||||
// a matching private key in ~/.ssh/. If no match is found, path is empty.
|
||||
func findSSHKey(client *gitea.Client) (string, error) {
|
||||
func findSSHKey(ctx stdctx.Context, client *gitea.Client) (string, error) {
|
||||
// get keys registered on gitea instance
|
||||
var keys []*gitea.PublicKey
|
||||
for page := 1; ; {
|
||||
page_keys, resp, err := client.ListMyPublicKeys(gitea.ListPublicKeysOptions{
|
||||
page_keys, resp, err := client.Users.ListMyPublicKeys(ctx, gitea.ListPublicKeysOptions{
|
||||
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -4,23 +4,24 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"code.gitea.io/tea/modules/print"
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"gitea.dev/tea/modules/config"
|
||||
"gitea.dev/tea/modules/print"
|
||||
)
|
||||
|
||||
// CreateMilestone creates a milestone in the given repo and prints the result
|
||||
func CreateMilestone(login *config.Login, repoOwner, repoName, title, description string, deadline *time.Time, state gitea.StateType) error {
|
||||
func CreateMilestone(ctx stdctx.Context, login *config.Login, repoOwner, repoName, title, description string, deadline *time.Time, state gitea.StateType) error {
|
||||
// title is required
|
||||
if len(title) == 0 {
|
||||
return fmt.Errorf("title is required")
|
||||
}
|
||||
|
||||
mile, _, err := login.Client().CreateMilestone(repoOwner, repoName, gitea.CreateMilestoneOption{
|
||||
mile, _, err := login.Client().Repositories.CreateMilestone(ctx, repoOwner, repoName, gitea.CreateMilestoneOption{
|
||||
Title: title,
|
||||
Description: description,
|
||||
Deadline: deadline,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -11,17 +12,16 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/modules/config"
|
||||
local_git "code.gitea.io/tea/modules/git"
|
||||
"code.gitea.io/tea/modules/utils"
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
git_plumbing "github.com/go-git/go-git/v5/plumbing"
|
||||
"gitea.dev/tea/modules/config"
|
||||
local_git "gitea.dev/tea/modules/git"
|
||||
"gitea.dev/tea/modules/utils"
|
||||
)
|
||||
|
||||
// PullCheckout checkout current workdir to the head branch of specified pull request
|
||||
func PullCheckout(
|
||||
ctx stdctx.Context,
|
||||
login *config.Login,
|
||||
repoOwner, repoName string,
|
||||
forceCreateBranch bool,
|
||||
@@ -29,7 +29,7 @@ func PullCheckout(
|
||||
callback func(string) (string, error),
|
||||
) error {
|
||||
client := login.Client()
|
||||
pr, _, err := client.GetPullRequest(repoOwner, repoName, index)
|
||||
pr, _, err := client.PullRequests.GetPullRequest(ctx, repoOwner, repoName, index)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't fetch PR: %s", err)
|
||||
}
|
||||
@@ -80,7 +80,7 @@ func doPRFetch(
|
||||
login *config.Login,
|
||||
pr *gitea.PullRequest,
|
||||
localRepo *local_git.TeaRepo,
|
||||
localRemote *git.Remote,
|
||||
localRemote *local_git.Remote,
|
||||
callback func(string) (string, error),
|
||||
) (string, error) {
|
||||
_ = callback
|
||||
@@ -90,6 +90,10 @@ func doPRFetch(
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
auth, err := local_git.GetAuthForURL(url, login.GetAccessToken(), login.SSHKey, callback)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
refspecs := []string{}
|
||||
if isRemoteDeleted(pr) {
|
||||
// When the head branch is already deleted, pr.Head.Ref points to
|
||||
@@ -97,15 +101,15 @@ func doPRFetch(
|
||||
// This ref must be fetched explicitly, and does not allow pushing, so we use it
|
||||
// only in this case as fallback.
|
||||
localBranchName = fmt.Sprintf("pulls/%d", pr.Index)
|
||||
refspecs = append(refspecs, fmt.Sprintf("%s:refs/remotes/%s/%s",
|
||||
refspecs = []string{fmt.Sprintf("%s:refs/remotes/%s/%s",
|
||||
pr.Head.Ref,
|
||||
localRemoteName,
|
||||
localBranchName,
|
||||
))
|
||||
)}
|
||||
}
|
||||
fmt.Printf("Fetching PR %v (head %s:%s) from remote '%s'\n", pr.Index, url, pr.Head.Ref, localRemoteName)
|
||||
|
||||
err = runGitFetch(localRemoteName, url.String(), login.GetAccessToken(), login.SSHKey, refspecs...)
|
||||
err = localRepo.Fetch(localRemoteName, refspecs, auth)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -122,12 +126,12 @@ func doPRCheckout(
|
||||
) error {
|
||||
// determine the ref to checkout, depending on existence of a matching commit on a local branch
|
||||
var info string
|
||||
var checkoutRef git_plumbing.ReferenceName
|
||||
var checkoutRef local_git.ReferenceName
|
||||
|
||||
if b, _ := localRepo.TeaFindBranchBySha(pr.Head.Sha, remoteURL); b != nil {
|
||||
|
||||
// if a matching branch exists, use that
|
||||
checkoutRef = git_plumbing.NewBranchReferenceName(b.Name)
|
||||
checkoutRef = local_git.NewBranchReferenceName(b.Name)
|
||||
info = fmt.Sprintf("Found matching local branch %s, checking it out", checkoutRef.Short())
|
||||
|
||||
} else if forceCreateBranch {
|
||||
@@ -137,10 +141,10 @@ func doPRCheckout(
|
||||
if isRemoteDeleted(pr) {
|
||||
localBranchName += "-" + pr.Head.Ref
|
||||
}
|
||||
checkoutRef = git_plumbing.NewBranchReferenceName(localBranchName)
|
||||
checkoutRef = local_git.NewBranchReferenceName(localBranchName)
|
||||
if err := localRepo.TeaCreateBranch(localBranchName, localRemoteBranchName, localRemoteName); err == nil {
|
||||
info = fmt.Sprintf("Created branch '%s'\n", localBranchName)
|
||||
} else if err == git.ErrBranchExists {
|
||||
} else if err == local_git.ErrBranchExists {
|
||||
info = "There may be changes since you last checked out, run `git pull` to get them."
|
||||
} else {
|
||||
return err
|
||||
@@ -149,7 +153,7 @@ func doPRCheckout(
|
||||
} else {
|
||||
|
||||
// use the remote tracking branch
|
||||
checkoutRef = git_plumbing.NewRemoteReferenceName(localRemoteName, localRemoteBranchName)
|
||||
checkoutRef = local_git.NewRemoteReferenceName(localRemoteName, localRemoteBranchName)
|
||||
info = fmt.Sprintf(
|
||||
"Checking out remote tracking branch %s. To make changes, create a new branch:\n git checkout %s",
|
||||
checkoutRef.String(), localRemoteBranchName)
|
||||
|
||||
@@ -4,21 +4,20 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/tea/modules/config"
|
||||
local_git "code.gitea.io/tea/modules/git"
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
git_config "github.com/go-git/go-git/v5/config"
|
||||
git_plumbing "github.com/go-git/go-git/v5/plumbing"
|
||||
"gitea.dev/tea/modules/config"
|
||||
local_git "gitea.dev/tea/modules/git"
|
||||
)
|
||||
|
||||
// PullClean deletes local & remote feature-branches for a closed pull
|
||||
func PullClean(login *config.Login, repoOwner, repoName string, index int64, ignoreSHA bool, callback func(string) (string, error)) error {
|
||||
func PullClean(ctx stdctx.Context, login *config.Login, repoOwner, repoName string, index int64, ignoreSHA bool, callback func(string) (string, error)) error {
|
||||
client := login.Client()
|
||||
|
||||
repo, _, err := client.GetRepo(repoOwner, repoName)
|
||||
repo, _, err := client.Repositories.GetRepo(ctx, repoOwner, repoName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -28,7 +27,7 @@ func PullClean(login *config.Login, repoOwner, repoName string, index int64, ign
|
||||
}
|
||||
|
||||
// fetch PR source-repo & -branch from gitea
|
||||
pr, _, err := client.GetPullRequest(repoOwner, repoName, index)
|
||||
pr, _, err := client.PullRequests.GetPullRequest(ctx, repoOwner, repoName, index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -51,7 +50,7 @@ func PullClean(login *config.Login, repoOwner, repoName string, index int64, ign
|
||||
}
|
||||
|
||||
// find a branch with matching sha or name, that has a remote matching the repo url
|
||||
var branch *git_config.Branch
|
||||
var branch *local_git.Branch
|
||||
if ignoreSHA {
|
||||
branch, err = r.TeaFindBranchByName(remoteBranch, pr.Head.Repository.CloneURL)
|
||||
} else {
|
||||
@@ -77,7 +76,7 @@ call me again with the --ignore-sha flag`, remoteBranch)
|
||||
}
|
||||
if headRef.Name().Short() == branch.Name {
|
||||
fmt.Printf("Checking out '%s' to delete local branch '%s'\n", defaultBranch, branch.Name)
|
||||
ref := git_plumbing.NewBranchReferenceName(defaultBranch)
|
||||
ref := local_git.NewBranchReferenceName(defaultBranch)
|
||||
if err = r.TeaCheckout(ref); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+16
-14
@@ -4,16 +4,18 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/modules/config"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
local_git "code.gitea.io/tea/modules/git"
|
||||
"code.gitea.io/tea/modules/print"
|
||||
"code.gitea.io/tea/modules/utils"
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"gitea.dev/tea/modules/config"
|
||||
"gitea.dev/tea/modules/context"
|
||||
local_git "gitea.dev/tea/modules/git"
|
||||
"gitea.dev/tea/modules/print"
|
||||
"gitea.dev/tea/modules/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -23,10 +25,10 @@ var (
|
||||
)
|
||||
|
||||
// CreatePull creates a PR in the given repo and prints the result
|
||||
func CreatePull(ctx *context.TeaContext, base, head string, allowMaintainerEdits *bool, opts *gitea.CreateIssueOption) (err error) {
|
||||
func CreatePull(requestCtx stdctx.Context, ctx *context.TeaContext, base, head string, allowMaintainerEdits *bool, opts *gitea.CreateIssueOption) (err error) {
|
||||
// default is default branch
|
||||
if len(base) == 0 {
|
||||
base, err = GetDefaultPRBase(ctx.Login, ctx.Owner, ctx.Repo)
|
||||
base, err = GetDefaultPRBase(requestCtx, ctx.Login, ctx.Owner, ctx.Repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -61,7 +63,7 @@ func CreatePull(ctx *context.TeaContext, base, head string, allowMaintainerEdits
|
||||
|
||||
client := ctx.Login.Client()
|
||||
|
||||
pr, _, err := client.CreatePullRequest(ctx.Owner, ctx.Repo, gitea.CreatePullRequestOption{
|
||||
pr, _, err := client.PullRequests.CreatePullRequest(requestCtx, ctx.Owner, ctx.Repo, gitea.CreatePullRequestOption{
|
||||
Head: head,
|
||||
Base: base,
|
||||
Title: opts.Title,
|
||||
@@ -76,7 +78,7 @@ func CreatePull(ctx *context.TeaContext, base, head string, allowMaintainerEdits
|
||||
}
|
||||
|
||||
if allowMaintainerEdits != nil && pr.AllowMaintainerEdit != *allowMaintainerEdits {
|
||||
pr, _, err = client.EditPullRequest(ctx.Owner, ctx.Repo, pr.Index, gitea.EditPullRequestOption{
|
||||
pr, _, err = client.PullRequests.EditPullRequest(requestCtx, ctx.Owner, ctx.Repo, pr.Index, gitea.EditPullRequestOption{
|
||||
AllowMaintainerEdit: allowMaintainerEdits,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -92,8 +94,8 @@ func CreatePull(ctx *context.TeaContext, base, head string, allowMaintainerEdits
|
||||
}
|
||||
|
||||
// GetDefaultPRBase retrieves the default base branch for the given repo
|
||||
func GetDefaultPRBase(login *config.Login, owner, repo string) (string, error) {
|
||||
meta, _, err := login.Client().GetRepo(owner, repo)
|
||||
func GetDefaultPRBase(requestCtx stdctx.Context, login *config.Login, owner, repo string) (string, error) {
|
||||
meta, _, err := login.Client().Repositories.GetRepo(requestCtx, owner, repo)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not fetch repo meta: %s", err)
|
||||
}
|
||||
@@ -155,13 +157,13 @@ func GetDefaultPRTitle(header string) string {
|
||||
}
|
||||
|
||||
// CreateAgitFlowPull creates a agit flow PR in the given repo and prints the result
|
||||
func CreateAgitFlowPull(ctx *context.TeaContext, remote, head, base, topic string,
|
||||
func CreateAgitFlowPull(requestCtx stdctx.Context, ctx *context.TeaContext, remote, head, base, topic string,
|
||||
opts *gitea.CreateIssueOption,
|
||||
callback func(string) (string, error),
|
||||
) (err error) {
|
||||
// default is default branch
|
||||
if len(base) == 0 {
|
||||
base, err = GetDefaultPRBase(ctx.Login, ctx.Owner, ctx.Repo)
|
||||
base, err = GetDefaultPRBase(requestCtx, ctx.Login, ctx.Owner, ctx.Repo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+11
-10
@@ -4,23 +4,24 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
gitea "gitea.dev/sdk"
|
||||
"gitea.dev/tea/modules/context"
|
||||
)
|
||||
|
||||
// EditPull edits a pull request and returns the updated pull request.
|
||||
func EditPull(ctx *context.TeaContext, client *gitea.Client, opts EditIssueOption) (*gitea.PullRequest, error) {
|
||||
func EditPull(requestCtx stdctx.Context, ctx *context.TeaContext, client *gitea.Client, opts EditIssueOption) (*gitea.PullRequest, error) {
|
||||
if client == nil {
|
||||
client = ctx.Login.Client()
|
||||
}
|
||||
|
||||
addLabelOpts, err := ResolveLabelOpts(client, ctx.Owner, ctx.Repo, opts.AddLabels)
|
||||
addLabelOpts, err := ResolveLabelOpts(requestCtx, client, ctx.Owner, ctx.Repo, opts.AddLabels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rmLabelOpts, err := ResolveLabelOpts(client, ctx.Owner, ctx.Repo, opts.RemoveLabels)
|
||||
rmLabelOpts, err := ResolveLabelOpts(requestCtx, client, ctx.Owner, ctx.Repo, opts.RemoveLabels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -36,7 +37,7 @@ func EditPull(ctx *context.TeaContext, client *gitea.Client, opts EditIssueOptio
|
||||
prOptsDirty = true
|
||||
}
|
||||
if opts.Milestone != nil {
|
||||
id, err := ResolveMilestoneID(client, ctx.Owner, ctx.Repo, *opts.Milestone)
|
||||
id, err := ResolveMilestoneID(requestCtx, client, ctx.Owner, ctx.Repo, *opts.Milestone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -55,22 +56,22 @@ func EditPull(ctx *context.TeaContext, client *gitea.Client, opts EditIssueOptio
|
||||
prOptsDirty = true
|
||||
}
|
||||
|
||||
if err := ApplyLabelChanges(client, ctx.Owner, ctx.Repo, opts.Index, addLabelOpts, rmLabelOpts); err != nil {
|
||||
if err := ApplyLabelChanges(requestCtx, client, ctx.Owner, ctx.Repo, opts.Index, addLabelOpts, rmLabelOpts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := ApplyReviewerChanges(client, ctx.Owner, ctx.Repo, opts.Index, opts.AddReviewers, opts.RemoveReviewers); err != nil {
|
||||
if err := ApplyReviewerChanges(requestCtx, client, ctx.Owner, ctx.Repo, opts.Index, opts.AddReviewers, opts.RemoveReviewers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var pr *gitea.PullRequest
|
||||
if prOptsDirty {
|
||||
pr, _, err = client.EditPullRequest(ctx.Owner, ctx.Repo, opts.Index, prOpts)
|
||||
pr, _, err = client.PullRequests.EditPullRequest(requestCtx, ctx.Owner, ctx.Repo, opts.Index, prOpts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not edit pull request: %s", err)
|
||||
}
|
||||
} else {
|
||||
pr, _, err = client.GetPullRequest(ctx.Owner, ctx.Repo, opts.Index)
|
||||
pr, _, err = client.PullRequests.GetPullRequest(requestCtx, ctx.Owner, ctx.Repo, opts.Index)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get pull request: %s", err)
|
||||
}
|
||||
|
||||
@@ -4,16 +4,18 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/modules/config"
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"gitea.dev/tea/modules/config"
|
||||
)
|
||||
|
||||
// PullMerge merges a PR
|
||||
func PullMerge(login *config.Login, repoOwner, repoName string, index int64, opt gitea.MergePullRequestOption) error {
|
||||
func PullMerge(requestCtx stdctx.Context, login *config.Login, repoOwner, repoName string, index int64, opt gitea.MergePullRequestOption) error {
|
||||
client := login.Client()
|
||||
success, _, err := client.MergePullRequest(repoOwner, repoName, index, opt)
|
||||
success, _, err := client.PullRequests.MergePullRequest(requestCtx, repoOwner, repoName, index, opt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,15 +4,16 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/tea/modules/context"
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
unidiff "gitea.com/noerw/unidiff-comments"
|
||||
"gitea.dev/tea/modules/context"
|
||||
)
|
||||
|
||||
var diffReviewHelp = `# This is the current diff of PR #%d on %s.
|
||||
@@ -28,10 +29,10 @@ var diffReviewHelp = `# This is the current diff of PR #%d on %s.
|
||||
`
|
||||
|
||||
// CreatePullReview submits a review for a PR
|
||||
func CreatePullReview(ctx *context.TeaContext, idx int64, status gitea.ReviewStateType, comment string, codeComments []gitea.CreatePullReviewComment) error {
|
||||
func CreatePullReview(requestCtx stdctx.Context, ctx *context.TeaContext, idx int64, status gitea.ReviewStateType, comment string, codeComments []gitea.CreatePullReviewComment) error {
|
||||
c := ctx.Login.Client()
|
||||
|
||||
review, _, err := c.CreatePullReview(ctx.Owner, ctx.Repo, idx, gitea.CreatePullReviewOptions{
|
||||
review, _, err := c.PullRequests.CreatePullReview(requestCtx, ctx.Owner, ctx.Repo, idx, gitea.CreatePullReviewOptions{
|
||||
State: status,
|
||||
Body: comment,
|
||||
Comments: codeComments,
|
||||
@@ -46,8 +47,8 @@ func CreatePullReview(ctx *context.TeaContext, idx int64, status gitea.ReviewSta
|
||||
|
||||
// SavePullDiff fetches the diff of a pull request and stores it as a temporary file.
|
||||
// The path to the file is returned.
|
||||
func SavePullDiff(ctx *context.TeaContext, idx int64) (string, error) {
|
||||
diff, _, err := ctx.Login.Client().GetPullRequestDiff(ctx.Owner, ctx.Repo, idx, gitea.PullRequestDiffOptions{})
|
||||
func SavePullDiff(requestCtx stdctx.Context, ctx *context.TeaContext, idx int64) (string, error) {
|
||||
diff, _, err := ctx.Login.Client().PullRequests.GetPullRequestDiff(requestCtx, ctx.Owner, ctx.Repo, idx, gitea.PullRequestDiffOptions{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -4,19 +4,21 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"gitea.dev/tea/modules/context"
|
||||
)
|
||||
|
||||
// ListPullReviewComments lists all review comments across all reviews for a PR
|
||||
func ListPullReviewComments(ctx *context.TeaContext, idx int64) ([]*gitea.PullReviewComment, error) {
|
||||
func ListPullReviewComments(requestCtx stdctx.Context, ctx *context.TeaContext, idx int64) ([]*gitea.PullReviewComment, error) {
|
||||
c := ctx.Login.Client()
|
||||
|
||||
var reviews []*gitea.PullReview
|
||||
for page := 1; ; {
|
||||
page_reviews, resp, err := c.ListPullReviews(ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{
|
||||
page_reviews, resp, err := c.PullRequests.ListPullReviews(requestCtx, ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -31,7 +33,7 @@ func ListPullReviewComments(ctx *context.TeaContext, idx int64) ([]*gitea.PullRe
|
||||
|
||||
var allComments []*gitea.PullReviewComment
|
||||
for _, review := range reviews {
|
||||
comments, _, err := c.ListPullReviewComments(ctx.Owner, ctx.Repo, idx, review.ID)
|
||||
comments, _, err := c.PullRequests.ListPullReviewComments(requestCtx, ctx.Owner, ctx.Repo, idx, review.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -42,10 +44,10 @@ func ListPullReviewComments(ctx *context.TeaContext, idx int64) ([]*gitea.PullRe
|
||||
}
|
||||
|
||||
// ResolvePullReviewComment resolves a review comment
|
||||
func ResolvePullReviewComment(ctx *context.TeaContext, commentID int64) error {
|
||||
func ResolvePullReviewComment(requestCtx stdctx.Context, ctx *context.TeaContext, commentID int64) error {
|
||||
c := ctx.Login.Client()
|
||||
|
||||
_, err := c.ResolvePullReviewComment(ctx.Owner, ctx.Repo, commentID)
|
||||
_, err := c.PullRequests.ResolvePullReviewComment(requestCtx, ctx.Owner, ctx.Repo, commentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -55,10 +57,10 @@ func ResolvePullReviewComment(ctx *context.TeaContext, commentID int64) error {
|
||||
}
|
||||
|
||||
// ReplyToPullReviewComment replies to a review comment on a pull request.
|
||||
func ReplyToPullReviewComment(ctx *context.TeaContext, idx, commentID int64, body string) error {
|
||||
func ReplyToPullReviewComment(requestCtx stdctx.Context, ctx *context.TeaContext, idx, commentID int64, body string) error {
|
||||
c := ctx.Login.Client()
|
||||
|
||||
comment, _, err := c.CreatePullReviewCommentReply(ctx.Owner, ctx.Repo, idx, commentID, gitea.CreatePullReviewCommentReplyOptions{
|
||||
comment, _, err := c.PullRequests.CreatePullReviewCommentReply(requestCtx, ctx.Owner, ctx.Repo, idx, commentID, gitea.CreatePullReviewCommentReplyOptions{
|
||||
Body: body,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -70,10 +72,10 @@ func ReplyToPullReviewComment(ctx *context.TeaContext, idx, commentID int64, bod
|
||||
}
|
||||
|
||||
// UnresolvePullReviewComment unresolves a review comment
|
||||
func UnresolvePullReviewComment(ctx *context.TeaContext, commentID int64) error {
|
||||
func UnresolvePullReviewComment(requestCtx stdctx.Context, ctx *context.TeaContext, commentID int64) error {
|
||||
c := ctx.Login.Client()
|
||||
|
||||
_, err := c.UnresolvePullReviewComment(ctx.Owner, ctx.Repo, commentID)
|
||||
_, err := c.PullRequests.UnresolvePullReviewComment(requestCtx, ctx.Owner, ctx.Repo, commentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+10
-30
@@ -4,28 +4,26 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
stdctx "context"
|
||||
"net/url"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/modules/config"
|
||||
local_git "code.gitea.io/tea/modules/git"
|
||||
gitea "gitea.dev/sdk"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
git_config "github.com/go-git/go-git/v5/config"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"gitea.dev/tea/modules/config"
|
||||
local_git "gitea.dev/tea/modules/git"
|
||||
)
|
||||
|
||||
// RepoClone creates a local git clone in the given path, and sets up upstream remote
|
||||
// for fork repos, for good usability with tea.
|
||||
func RepoClone(
|
||||
ctx stdctx.Context,
|
||||
path string,
|
||||
login *config.Login,
|
||||
repoOwner, repoName string,
|
||||
callback func(string) (string, error),
|
||||
depth int,
|
||||
) (*local_git.TeaRepo, error) {
|
||||
repoMeta, _, err := login.Client().GetRepo(repoOwner, repoName)
|
||||
repoMeta, _, err := login.Client().Repositories.GetRepo(ctx, repoOwner, repoName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -45,12 +43,7 @@ func RepoClone(
|
||||
path = repoName
|
||||
}
|
||||
|
||||
repo, err := git.PlainClone(path, false, &git.CloneOptions{
|
||||
URL: originURL.String(),
|
||||
Auth: auth,
|
||||
Depth: depth,
|
||||
InsecureSkipTLS: login.Insecure,
|
||||
})
|
||||
repo, err := local_git.Clone(path, originURL.String(), auth, depth, login.Insecure)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -62,28 +55,15 @@ func RepoClone(
|
||||
return nil, err
|
||||
}
|
||||
upstreamBranch := repoMeta.Parent.DefaultBranch
|
||||
_, err = repo.CreateRemote(&git_config.RemoteConfig{
|
||||
Name: "upstream",
|
||||
URLs: []string{upstreamURL.String()},
|
||||
})
|
||||
if err != nil {
|
||||
if err = repo.AddRemote("upstream", upstreamURL.String()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repoConf, err := repo.Config()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if b, ok := repoConf.Branches[upstreamBranch]; ok {
|
||||
b.Remote = "upstream"
|
||||
b.Merge = plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", upstreamBranch))
|
||||
}
|
||||
if err = repo.SetConfig(repoConf); err != nil {
|
||||
if err = repo.SetBranchUpstream(upstreamBranch, "upstream", upstreamBranch); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &local_git.TeaRepo{Repository: repo}, nil
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
func cloneURL(repo *gitea.Repository, login *config.Login) (*url.URL, error) {
|
||||
|
||||
Reference in New Issue
Block a user