Merge branch 'main' into lunny/add_reply_code_review

This commit is contained in:
Lunny Xiao
2026-05-25 21:58:27 -07:00
230 changed files with 2346 additions and 1495 deletions
+2 -2
View File
@@ -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
View File
@@ -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
}
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"sync"
"time"
"code.gitea.io/tea/modules/filelock"
"gitea.dev/tea/modules/filelock"
)
const (
+8 -9
View File
@@ -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},
-2
View File
@@ -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
View File
@@ -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)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -6,7 +6,7 @@ package context
import (
"testing"
"code.gitea.io/tea/modules/config"
"gitea.dev/tea/modules/config"
)
func TestShouldPromptFallbackLogin(t *testing.T) {
+3 -3
View File
@@ -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
+3 -3
View File
@@ -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
+3 -2
View File
@@ -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) {
+1 -1
View File
@@ -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
View File
@@ -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, "'", "'\\''") + "'"
}
+103
View File
@@ -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()
}
+74
View File
@@ -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
View File
@@ -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)
}
+374
View File
@@ -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
}
+28
View File
@@ -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
View File
@@ -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
View File
@@ -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])
}
+136
View File
@@ -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)
}
+163
View File
@@ -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)
}
+1 -1
View File
@@ -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.
+12 -10
View File
@@ -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)
+16 -14
View File
@@ -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 {
+10 -9
View File
@@ -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().
+10 -8
View File
@@ -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{
+7 -7
View File
@@ -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,
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"fmt"
"os"
"code.gitea.io/tea/modules/theme"
"gitea.dev/tea/modules/theme"
"charm.land/lipgloss/v2"
)
+2 -2
View File
@@ -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"
)
+12 -8
View File
@@ -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,
+12 -10
View File
@@ -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
}
+12 -10
View File
@@ -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
}
+1 -1
View File
@@ -6,7 +6,7 @@ package print
import (
"fmt"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
)
// ActionSecretsList prints a list of action secrets
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"fmt"
"time"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
)
// formatDurationMinutes formats duration in a human-readable way
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"testing"
"time"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
"github.com/stretchr/testify/require"
)
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"testing"
"time"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
"github.com/stretchr/testify/require"
)
+1 -1
View File
@@ -6,7 +6,7 @@ package print
import (
"fmt"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
)
func formatByteSize(size int64) string {
+1 -1
View File
@@ -6,7 +6,7 @@ package print
import (
"fmt"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
)
// BranchesList prints a listing of the branches
+1 -1
View File
@@ -6,7 +6,7 @@ package print
import (
"testing"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
"github.com/stretchr/testify/assert"
)
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"fmt"
"strings"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
)
// Comments renders a list of comments to stdout
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"regexp"
"time"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
"github.com/muesli/termenv"
"golang.org/x/term"
)
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"fmt"
"strings"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
"github.com/enescakir/emoji"
)
+1 -1
View File
@@ -6,7 +6,7 @@ package print
import (
"strconv"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
)
// LabelsList prints a listing of labels
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"strings"
"time"
"code.gitea.io/tea/modules/config"
"gitea.dev/tea/modules/config"
)
// LoginDetails print login entry to stdout
+1 -1
View File
@@ -6,7 +6,7 @@ package print
import (
"fmt"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
)
// MilestoneDetails print an milestone formatted to stdout
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"fmt"
"strings"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
)
// NotificationsList prints a listing of notification threads
+1 -1
View File
@@ -6,7 +6,7 @@ package print
import (
"fmt"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
)
// OrganizationDetails prints details of an org with formatting
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"fmt"
"strings"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
)
var ciStatusSymbols = map[gitea.StatusState]string{
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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"
)
+1 -1
View File
@@ -4,7 +4,7 @@
package print
import (
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
)
// ReleasesList prints a listing of releases
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"strings"
"time"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
)
// ReposList prints a listing of the repos
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"encoding/json"
"testing"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
"github.com/stretchr/testify/require"
)
+1 -1
View File
@@ -6,7 +6,7 @@ package print
import (
"fmt"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
)
// SSHKeysList prints a table of SSH public keys
+1 -1
View File
@@ -6,7 +6,7 @@ package print
import (
"fmt"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
)
// TrackedTimesList print list of tracked times to stdout
+1 -1
View File
@@ -6,7 +6,7 @@ package print
import (
"fmt"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
)
// UserDetails print a formatted user to stdout
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"strconv"
"strings"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
)
// WebhooksList prints a listing of webhooks
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"testing"
"time"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
"github.com/stretchr/testify/assert"
)
+7 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"fmt"
"os"
"code.gitea.io/sdk/gitea"
"gitea.dev/sdk"
)
// LabelsExport save list of labels to disc
+11 -10
View File
@@ -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,
})
+3 -2
View File
@@ -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
+5 -4
View File
@@ -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 {
+6 -5
View File
@@ -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,
+20 -16
View File
@@ -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)
+9 -10
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+6 -4
View File
@@ -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
}
+7 -6
View File
@@ -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
}
+13 -11
View File
@@ -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
View File
@@ -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) {