revert: tea 1.0 rewrite (#1122–#1126) (#1146)

Reverts #1126, #1125, #1124, #1123 and #1122.

`go.mod` keeps the Go 1.26 line with toolchain `go1.26.9` (instead of the original `go1.26.6`) so the security fix from #1131 isn't lost.

reopens #69
reopens #141
reopens #162
reopens #172
reopens #197
reopens #232
reopens #240
reopens #246
reopens #323
reopens #325
reopens #326
reopens #328
reopens #344
reopens #351
reopens #373
reopens #403
reopens #406
reopens #417
reopens #430
reopens #457
reopens #480
reopens #482
reopens #489
reopens #490
reopens #498
reopens #500
reopens #504
reopens #510
reopens #516
reopens #523
reopens #556
reopens #560
reopens #569
reopens #571
reopens #602
reopens #632
reopens #633
reopens #643
reopens #666
reopens #670
reopens #672
reopens #677
reopens #683
reopens #697
reopens #726
reopens #737
reopens #770
reopens #779
reopens #804
reopens #817
reopens #831
reopens #833
reopens #834
reopens #908
reopens #966
reopens #1023
reopens #1029
reopens #1032
reopens #1050
reopens #1052
reopens #1069
reopens #1073
reopens #1087
reopens #1091
reopens #1094
reopens #1110
reopens #1115
reopens #1116
reopens #1118

Reviewed-on: https://gitea.com/gitea/tea/pulls/1146
Reviewed-by: TheFox0x7 <95654+thefox0x7@noreply.gitea.com>
This commit is contained in:
bircni committed 2026-10-18 20:56:33 +00:00
1 parent 33bb12729e
commit c5f674111a
604 files changed
+33711 -35545

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issues
import (
stdctx "context"
"fmt"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"gitea.dev/tea/modules/utils"
"github.com/urfave/cli/v3"
)
// CmdIssuesClose represents a sub command of issues to close an issue
var CmdIssuesClose = cli.Command{
Name: "close",
Usage: "Change state of one ore more issues to 'closed'",
Description: `Change state of one ore more issues to 'closed'`,
ArgsUsage: "<issue index> [<issue index>...]",
Action: func(ctx stdctx.Context, cmd *cli.Command) error {
s := gitea.StateClosed
return editIssueState(ctx, cmd, gitea.EditIssueOption{State: &s})
},
Flags: flags.AllDefaultFlags,
}
// editIssueState abstracts the arg parsing to edit the given issue
func editIssueState(requestCtx stdctx.Context, cmd *cli.Command, opts gitea.EditIssueOption) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if ctx.Args().Len() == 0 {
return fmt.Errorf("missing required argument: %s", ctx.Command.ArgsUsage)
}
indices, err := utils.ArgsToIndices(ctx.Args().Slice())
if err != nil {
return err
}
client := ctx.Login.Client()
for _, index := range indices {
issue, _, err := client.Issues.EditIssue(requestCtx, ctx.Owner, ctx.Repo, index, opts)
if err != nil {
return err
}
if len(indices) > 1 {
fmt.Println(issue.HTMLURL)
} else {
print.IssueDetails(issue, nil)
}
}
return nil
}
+95
View File
@@ -0,0 +1,95 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issues
import (
stdctx "context"
"encoding/json"
"fmt"
"io"
gitea "gitea.dev/sdk"
"github.com/urfave/cli/v3"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/interact"
"gitea.dev/tea/modules/print"
"gitea.dev/tea/modules/task"
)
// CmdIssuesCreate represents a sub command of issues to create issue
var CmdIssuesCreate = cli.Command{
Name: "create",
Aliases: []string{"c"},
Usage: "Create an issue on repository",
Description: `Create an issue on repository`,
ArgsUsage: " ", // command does not accept arguments
Action: runIssuesCreate,
Flags: flags.IssuePRCreateFlags,
}
func runIssuesCreate(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if ctx.IsInteractiveMode() {
err := interact.CreateIssue(requestCtx, ctx.Login, ctx.Owner, ctx.Repo)
if err != nil && !interact.IsQuitting(err) {
return err
}
return nil
}
opts, err := flags.GetIssuePRCreateFlags(requestCtx, ctx)
if err != nil {
return err
}
issue, err := task.CreateIssue(requestCtx, ctx.Login,
ctx.Owner,
ctx.Repo,
*opts,
)
if err != nil {
return err
}
if ctx.IsSet("output") {
switch ctx.String("output") {
case "json":
return writeCreatedIssueAsJSON(ctx.Writer, issue)
}
}
print.IssueDetails(issue, nil)
fmt.Println(issue.HTMLURL)
return nil
}
// createdIssueJSON is the machine-readable representation of a freshly
// created issue, mirroring the create-PR equivalent in cmd/pulls/create.go
// (createdPullJSON).
type createdIssueJSON struct {
Index int64 `json:"index"`
Title string `json:"title"`
URL string `json:"url"`
State gitea.StateType `json:"state"`
}
func writeCreatedIssueAsJSON(w io.Writer, issue *gitea.Issue) error {
return json.NewEncoder(w).Encode(createdIssueJSON{
Index: issue.Index,
Title: issue.Title,
URL: issue.HTMLURL,
State: issue.State,
})
}
+41
View File
@@ -0,0 +1,41 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issues
import (
"bytes"
"encoding/json"
"testing"
gitea "gitea.dev/sdk"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWriteCreatedIssueAsJSON(t *testing.T) {
issue := &gitea.Issue{
Index: 42,
Title: "test title",
HTMLURL: "https://gitea.example.com/owner/repo/issues/42",
State: gitea.StateOpen,
}
var buf bytes.Buffer
require.NoError(t, writeCreatedIssueAsJSON(&buf, issue))
var got map[string]any
require.NoError(t, json.Unmarshal(buf.Bytes(), &got))
assert.Equal(t, float64(42), got["index"])
assert.Equal(t, "test title", got["title"])
assert.Equal(t, "https://gitea.example.com/owner/repo/issues/42", got["url"])
assert.Equal(t, "open", got["state"])
// exactly the lean field set, nothing extra
assert.Len(t, got, 4)
// machine-readable output must not contain terminal escape sequences
assert.NotContains(t, buf.String(), "\x1b")
}
+81
View File
@@ -0,0 +1,81 @@
// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issues
import (
"fmt"
stdctx "context"
"github.com/urfave/cli/v3"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/interact"
"gitea.dev/tea/modules/print"
"gitea.dev/tea/modules/task"
"gitea.dev/tea/modules/utils"
)
// CmdIssuesEdit is the subcommand of issues to edit issues
var CmdIssuesEdit = cli.Command{
Name: "edit",
Aliases: []string{"e"},
Usage: "Edit one or more issues",
Description: `Edit one or more issues. To unset a property again,
use an empty string (eg. --milestone "").`,
ArgsUsage: "<idx> [<idx>...]",
Action: runIssuesEdit,
Flags: flags.IssuePREditFlags,
}
func runIssuesEdit(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
return err
}
if !cmd.Args().Present() {
return fmt.Errorf("must specify at least one issue index")
}
opts, err := flags.GetIssuePREditFlags(ctx)
if err != nil {
return err
}
indices, err := utils.ArgsToIndices(ctx.Args().Slice())
if err != nil {
return err
}
client := ctx.Login.Client()
for _, opts.Index = range indices {
if ctx.IsInteractiveMode() {
var err error
opts, err = interact.EditIssue(requestCtx, *ctx, opts.Index)
if err != nil {
if interact.IsQuitting(err) {
return nil // user quit
}
return err
}
}
issue, err := task.EditIssue(requestCtx, ctx, client, *opts)
if err != nil {
return err
}
if ctx.Args().Len() > 1 {
fmt.Println(issue.HTMLURL)
} else {
print.IssueDetails(issue, nil)
}
}
return nil
}
+119
View File
@@ -0,0 +1,119 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issues
import (
stdctx "context"
"errors"
"time"
gitea "gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"gitea.dev/tea/modules/context"
"gitea.dev/tea/modules/print"
"github.com/araddon/dateparse"
"github.com/urfave/cli/v3"
)
var issueFieldsFlag = flags.FieldsFlag(print.IssueFields, []string{
"index", "title", "state", "author", "milestone", "labels", "owner", "repo",
})
// CmdIssuesList represents a sub command of issues to list issues
var CmdIssuesList = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "List issues of the repository",
Description: `List issues of the repository`,
ArgsUsage: " ", // command does not accept arguments
Action: RunIssuesList,
Flags: append([]cli.Flag{issueFieldsFlag}, flags.IssueListingFlags...),
}
// RunIssuesList list issues
func RunIssuesList(requestCtx stdctx.Context, cmd *cli.Command) error {
ctx, err := context.InitCommand(cmd)
if err != nil {
return err
}
state, err := flags.ParseState(ctx.String("state"))
if err != nil {
return err
}
kind, err := flags.ParseIssueKind(ctx.String("kind"), gitea.IssueTypeIssue)
if err != nil {
return err
}
var from, until time.Time
if ctx.IsSet("from") {
from, err = dateparse.ParseLocal(ctx.String("from"))
if err != nil {
return err
}
}
if ctx.IsSet("until") {
until, err = dateparse.ParseLocal(ctx.String("until"))
if err != nil {
return err
}
}
owner := ctx.Owner
if ctx.IsSet("owner") {
owner = ctx.String("owner")
}
// ignore error, as we don't do any input validation on these flags
labels, _ := flags.LabelFilterFlag.GetValues(cmd)
milestones, _ := flags.MilestoneFilterFlag.GetValues(cmd)
var issues []*gitea.Issue
if ctx.Repo != "" {
issues, _, err = ctx.Login.Client().Issues.ListRepoIssues(requestCtx, owner, ctx.Repo, gitea.ListIssueOption{
ListOptions: flags.GetListOptions(cmd),
State: state,
Type: kind,
KeyWord: ctx.String("keyword"),
CreatedBy: ctx.String("author"),
AssignedBy: ctx.String("assignee"),
MentionedBy: ctx.String("mentions"),
Labels: labels,
Milestones: milestones,
Since: from,
Before: until,
})
if err != nil {
return err
}
} else {
if ctx.IsSet("assignee") {
return errors.New("--assignee requires --repo (global issue search does not support assignee filter)")
}
issues, _, err = ctx.Login.Client().Issues.ListIssues(requestCtx, gitea.ListIssueOption{
ListOptions: flags.GetListOptions(cmd),
State: state,
Type: kind,
KeyWord: ctx.String("keyword"),
CreatedBy: ctx.String("author"),
MentionedBy: ctx.String("mentions"),
Labels: labels,
Milestones: milestones,
Since: from,
Before: until,
Owner: owner,
})
if err != nil {
return err
}
}
fields, err := issueFieldsFlag.GetValues(cmd)
if err != nil {
return err
}
return print.IssuesPullsList(issues, ctx.Output, fields)
}
+115
View File
@@ -0,0 +1,115 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issues
import (
stdctx "context"
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
"gitea.dev/tea/modules/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
"golang.org/x/crypto/ssh"
)
func TestRunIssuesListWithSSHPubkeyLoginDoesNotDeadlock(t *testing.T) {
t.Parallel()
sshKeyPath, fingerprint := writeTestSSHKey(t)
var versionRequests atomic.Int32
var issueRequests atomic.Int32
var signedVersionRequests atomic.Int32
var signedIssueRequests atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/version":
versionRequests.Add(1)
if r.Header.Get("Signature") != "" {
signedVersionRequests.Add(1)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"version":"1.26.4"}`))
case "/api/v1/repos/gitea/tea/issues":
issueRequests.Add(1)
if r.Header.Get("Signature") != "" {
signedIssueRequests.Add(1)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`[]`))
default:
t.Errorf("unexpected path %s", r.URL.Path)
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
config.SetConfigForTesting(config.LocalConfig{
Logins: []config.Login{{
Name: "ssh-login",
URL: server.URL,
SSHKey: sshKeyPath,
SSHKeyFingerprint: fingerprint,
VersionCheck: true,
Default: true,
}},
})
cmd := cli.Command{
Name: CmdIssuesList.Name,
Flags: CmdIssuesList.Flags,
}
require.NoError(t, cmd.Set("login", "ssh-login"))
require.NoError(t, cmd.Set("repo", "gitea/tea"))
require.NoError(t, cmd.Set("output", "json"))
done := make(chan error, 1)
go func() {
done <- RunIssuesList(stdctx.Background(), &cmd)
}()
select {
case err := <-done:
require.NoError(t, err)
case <-time.After(2 * time.Second):
t.Fatal("RunIssuesList deadlocked while bootstrapping the server version for HTTPSign authentication")
}
assert.EqualValues(t, 1, versionRequests.Load())
assert.EqualValues(t, 0, signedVersionRequests.Load())
assert.EqualValues(t, 1, issueRequests.Load())
assert.EqualValues(t, 1, signedIssueRequests.Load())
}
func writeTestSSHKey(t *testing.T) (string, string) {
t.Helper()
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
pkcs8, err := x509.MarshalPKCS8PrivateKey(privateKey)
require.NoError(t, err)
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8})
sshKeyPath := filepath.Join(t.TempDir(), "id_ed25519")
require.NoError(t, os.WriteFile(sshKeyPath, pemBytes, 0o600))
signer, err := ssh.NewSignerFromKey(privateKey)
require.NoError(t, err)
return sshKeyPath, ssh.FingerprintSHA256(signer.PublicKey())
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issues
import (
"context"
"gitea.dev/sdk"
"gitea.dev/tea/cmd/flags"
"github.com/urfave/cli/v3"
)
// CmdIssuesReopen represents a sub command of issues to open an issue
var CmdIssuesReopen = cli.Command{
Name: "reopen",
Aliases: []string{"open"},
Usage: "Change state of one or more issues to 'open'",
Description: `Change state of one or more issues to 'open'`,
ArgsUsage: "<issue index> [<issue index>...]",
Action: func(ctx context.Context, cmd *cli.Command) error {
s := gitea.StateOpen
return editIssueState(ctx, cmd, gitea.EditIssueOption{State: &s})
},
Flags: flags.AllDefaultFlags,
}