Code Cleanup (#869)

- switch to golangci-lint for linting
- switch to gofmpt for formatting
- fix lint and fmt issues that came up from switch to new tools
- upgrade go-sdk to 0.23.2
- support pagination for listing tracked times
- remove `FixPullHeadSha` workaround (upstream fix has been merged for 5+ years at this point)
- standardize on US spelling (previously a mix of US&UK spelling)
- remove some unused code
- reduce some duplication in parsing state and issue type
- reduce some duplication in reading input for secrets and variables
- reduce some duplication with PR Review code
- report error for when yaml parsing fails
- various other misc cleanup

Reviewed-on: https://gitea.com/gitea/tea/pulls/869
Co-authored-by: techknowlogick <techknowlogick@gitea.com>
Co-committed-by: techknowlogick <techknowlogick@gitea.com>
This commit is contained in:
techknowlogick
2026-02-02 22:39:26 +00:00
committed by techknowlogick
parent ae740a66e8
commit 20da414145
62 changed files with 399 additions and 356 deletions

View File

@@ -30,7 +30,6 @@ jobs:
make vet make vet
make lint make lint
make fmt-check make fmt-check
make misspell-check
make docs-check make docs-check
make build make build
- run: curl --noproxy "*" http://gitea:3000/api/v1/version # verify connection to instance - run: curl --noproxy "*" http://gitea:3000/api/v1/version # verify connection to instance

45
.golangci.yml Normal file
View File

@@ -0,0 +1,45 @@
version: "2"
formatters:
enable:
- gofumpt
linters:
default: none
enable:
- govet
- revive
- misspell
- ineffassign
- unused
settings:
revive:
rules:
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-strings
- name: error-naming
- name: exported
- name: if-return
- name: increment-decrement
- name: var-declaration
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: indent-error-flow
- name: errorf
misspell:
locale: US
ignore-words:
- unknwon
- destory
issues:
max-issues-per-linter: 0
max-same-issues: 0

View File

@@ -5,7 +5,10 @@ SHASUM ?= shasum -a 256
export PATH := $($(GO) env GOPATH)/bin:$(PATH) export PATH := $($(GO) env GOPATH)/bin:$(PATH)
GOFILES := $(shell find . -name "*.go" -type f ! -path "*/bindata.go") GOFILES := $(shell find . -name "*.go" -type f ! -path "*/bindata.go")
GOFMT ?= gofmt -s
# Tool packages with pinned versions
GOFUMPT_PACKAGE ?= mvdan.cc/gofumpt@v0.7.0
GOLANGCI_LINT_PACKAGE ?= github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.8.0
ifneq ($(DRONE_TAG),) ifneq ($(DRONE_TAG),)
VERSION ?= $(subst v,,$(DRONE_TAG)) VERSION ?= $(subst v,,$(DRONE_TAG))
@@ -49,7 +52,7 @@ clean:
.PHONY: fmt .PHONY: fmt
fmt: fmt:
$(GOFMT) -w $(GOFILES) $(GO) run $(GOFUMPT_PACKAGE) -w $(GOFILES)
.PHONY: vet .PHONY: vet
vet: vet:
@@ -60,21 +63,17 @@ vet:
$(GO) vet -vettool=$(VET_TOOL) $(PACKAGES) $(GO) vet -vettool=$(VET_TOOL) $(PACKAGES)
.PHONY: lint .PHONY: lint
lint: install-lint-tools lint:
$(GO) run github.com/mgechev/revive@v1.3.2 -config .revive.toml ./... || exit 1 $(GO) run $(GOLANGCI_LINT_PACKAGE) run
.PHONY: misspell-check .PHONY: lint-fix
misspell-check: install-lint-tools lint-fix:
$(GO) run github.com/client9/misspell/cmd/misspell@latest -error -i unknwon,destory $(GOFILES) $(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix
.PHONY: misspell
misspell: install-lint-tools
$(GO) run github.com/client9/misspell/cmd/misspell@latest -w -i unknwon $(GOFILES)
.PHONY: fmt-check .PHONY: fmt-check
fmt-check: fmt-check:
# get all go files and run go fmt on them # get all go files and run gofumpt on them
@diff=$$($(GOFMT) -d $(GOFILES)); \ @diff=$$($(GO) run $(GOFUMPT_PACKAGE) -d $(GOFILES)); \
if [ -n "$$diff" ]; then \ if [ -n "$$diff" ]; then \
echo "Please run 'make fmt' and commit the result:"; \ echo "Please run 'make fmt' and commit the result:"; \
echo "$${diff}"; \ echo "$${diff}"; \
@@ -124,10 +123,3 @@ $(EXECUTABLE): $(SOURCES)
build-image: build-image:
docker build --build-arg VERSION=$(TEA_VERSION) -t gitea/tea:$(TEA_VERSION_TAG) . docker build --build-arg VERSION=$(TEA_VERSION) -t gitea/tea:$(TEA_VERSION_TAG) .
install-lint-tools:
@hash revive > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) install github.com/mgechev/revive@v1.3.2; \
fi
@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
$(GO) install github.com/client9/misspell/cmd/misspell@latest; \
fi

View File

@@ -6,17 +6,13 @@ package secrets
import ( import (
stdctx "context" stdctx "context"
"fmt" "fmt"
"io"
"os"
"strings"
"syscall"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"golang.org/x/term"
) )
// CmdSecretsCreate represents a sub command to create action secrets // CmdSecretsCreate represents a sub command to create action secrets
@@ -48,42 +44,19 @@ func runSecretsCreate(ctx stdctx.Context, cmd *cli.Command) error {
client := c.Login.Client() client := c.Login.Client()
secretName := cmd.Args().First() secretName := cmd.Args().First()
var secretValue string
// Determine how to get the secret value // Read secret value using the utility
if cmd.String("file") != "" { secretValue, err := utils.ReadValue(cmd, utils.ReadValueOptions{
// Read from file ResourceName: "secret",
content, err := os.ReadFile(cmd.String("file")) PromptMsg: fmt.Sprintf("Enter secret value for '%s'", secretName),
Hidden: true,
AllowEmpty: false,
})
if err != nil { if err != nil {
return fmt.Errorf("failed to read file: %w", err) return err
}
secretValue = strings.TrimSpace(string(content))
} else if cmd.Bool("stdin") {
// Read from stdin
content, err := io.ReadAll(os.Stdin)
if err != nil {
return fmt.Errorf("failed to read from stdin: %w", err)
}
secretValue = strings.TrimSpace(string(content))
} else if cmd.Args().Len() >= 2 {
// Use provided argument
secretValue = cmd.Args().Get(1)
} else {
// Interactive prompt (hidden input)
fmt.Printf("Enter secret value for '%s': ", secretName)
byteValue, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return fmt.Errorf("failed to read secret value: %w", err)
}
fmt.Println() // Add newline after hidden input
secretValue = string(byteValue)
} }
if secretValue == "" { _, err = client.CreateRepoActionSecret(c.Owner, c.Repo, gitea.CreateSecretOption{
return fmt.Errorf("secret value cannot be empty")
}
_, err := client.CreateRepoActionSecret(c.Owner, c.Repo, gitea.CreateSecretOption{
Name: secretName, Name: secretName,
Data: secretValue, Data: secretValue,
}) })

View File

@@ -45,7 +45,7 @@ func runSecretsDelete(ctx stdctx.Context, cmd *cli.Command) error {
var response string var response string
fmt.Scanln(&response) fmt.Scanln(&response)
if response != "y" && response != "Y" && response != "yes" { if response != "y" && response != "Y" && response != "yes" {
fmt.Println("Deletion cancelled.") fmt.Println("Deletion canceled.")
return nil return nil
} }
} }

View File

@@ -45,7 +45,7 @@ func runVariablesDelete(ctx stdctx.Context, cmd *cli.Command) error {
var response string var response string
fmt.Scanln(&response) fmt.Scanln(&response)
if response != "y" && response != "Y" && response != "yes" { if response != "y" && response != "Y" && response != "yes" {
fmt.Println("Deletion cancelled.") fmt.Println("Deletion canceled.")
return nil return nil
} }
} }

View File

@@ -6,13 +6,11 @@ package variables
import ( import (
stdctx "context" stdctx "context"
"fmt" "fmt"
"io"
"os"
"regexp" "regexp"
"strings"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@@ -46,39 +44,26 @@ func runVariablesSet(ctx stdctx.Context, cmd *cli.Command) error {
client := c.Login.Client() client := c.Login.Client()
variableName := cmd.Args().First() variableName := cmd.Args().First()
var variableValue string if err := validateVariableName(variableName); err != nil {
return err
}
// Determine how to get the variable value // Read variable value using the utility
if cmd.String("file") != "" { variableValue, err := utils.ReadValue(cmd, utils.ReadValueOptions{
// Read from file ResourceName: "variable",
content, err := os.ReadFile(cmd.String("file")) PromptMsg: fmt.Sprintf("Enter variable value for '%s'", variableName),
Hidden: false,
AllowEmpty: false,
})
if err != nil { if err != nil {
return fmt.Errorf("failed to read file: %w", err) return err
}
variableValue = strings.TrimSpace(string(content))
} else if cmd.Bool("stdin") {
// Read from stdin
content, err := io.ReadAll(os.Stdin)
if err != nil {
return fmt.Errorf("failed to read from stdin: %w", err)
}
variableValue = strings.TrimSpace(string(content))
} else if cmd.Args().Len() >= 2 {
// Use provided argument
variableValue = cmd.Args().Get(1)
} else {
// Interactive prompt
fmt.Printf("Enter variable value for '%s': ", variableName)
var input string
fmt.Scanln(&input)
variableValue = input
} }
if variableValue == "" { if err := validateVariableValue(variableValue); err != nil {
return fmt.Errorf("variable value cannot be empty") return err
} }
_, err := client.CreateRepoActionVariable(c.Owner, c.Repo, variableName, variableValue) _, err = client.CreateRepoActionVariable(c.Owner, c.Repo, variableName, variableValue)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -81,21 +81,3 @@ func runReleaseAttachmentDelete(_ stdctx.Context, cmd *cli.Command) error {
return nil return nil
} }
func getReleaseAttachmentByName(owner, repo string, release int64, name string, client *gitea.Client) (*gitea.Attachment, error) {
al, _, err := client.ListReleaseAttachments(owner, repo, release, gitea.ListReleaseAttachmentsOptions{
ListOptions: gitea.ListOptions{Page: -1},
})
if err != nil {
return nil, err
}
if len(al) == 0 {
return nil, fmt.Errorf("Release does not have any attachments")
}
for _, a := range al {
if a.Name == name {
return a, nil
}
}
return nil, fmt.Errorf("Attachment does not exist")
}

View File

@@ -52,7 +52,6 @@ func RunBranchesList(_ stdctx.Context, cmd *cli.Command) error {
branches, _, err = ctx.Login.Client().ListRepoBranches(owner, ctx.Repo, gitea.ListRepoBranchesOptions{ branches, _, err = ctx.Login.Client().ListRepoBranches(owner, ctx.Repo, gitea.ListRepoBranchesOptions{
ListOptions: flags.GetListOptions(), ListOptions: flags.GetListOptions(),
}) })
if err != nil { if err != nil {
return err return err
} }
@@ -60,7 +59,6 @@ func RunBranchesList(_ stdctx.Context, cmd *cli.Command) error {
protections, _, err = ctx.Login.Client().ListBranchProtections(owner, ctx.Repo, gitea.ListBranchProtectionsOptions{ protections, _, err = ctx.Login.Client().ListBranchProtections(owner, ctx.Repo, gitea.ListBranchProtectionsOptions{
ListOptions: flags.GetListOptions(), ListOptions: flags.GetListOptions(),
}) })
if err != nil { if err != nil {
return err return err
} }

View File

@@ -58,7 +58,7 @@ func runRepoClone(ctx stdctx.Context, cmd *cli.Command) error {
var ( var (
login *config.Login = teaCmd.Login login *config.Login = teaCmd.Login
owner string = teaCmd.Login.User owner string
repo string repo string
) )

View File

@@ -141,3 +141,34 @@ var NotificationStateFlag = NewCsvFlag(
func FieldsFlag(availableFields, defaultFields []string) *CsvFlag { func FieldsFlag(availableFields, defaultFields []string) *CsvFlag {
return NewCsvFlag("fields", "fields to print", []string{"f"}, availableFields, defaultFields) return NewCsvFlag("fields", "fields to print", []string{"f"}, availableFields, defaultFields)
} }
// ParseState parses a state string and returns the corresponding gitea.StateType
func ParseState(stateStr string) (gitea.StateType, error) {
switch stateStr {
case "all":
return gitea.StateAll, nil
case "", "open":
return gitea.StateOpen, nil
case "closed":
return gitea.StateClosed, nil
default:
return "", errors.New("unknown state '" + stateStr + "'")
}
}
// ParseIssueKind parses a kind string and returns the corresponding gitea.IssueType.
// If kindStr is empty, returns the provided defaultKind.
func ParseIssueKind(kindStr string, defaultKind gitea.IssueType) (gitea.IssueType, error) {
switch kindStr {
case "":
return defaultKind, nil
case "all":
return gitea.IssueTypeAll, nil
case "issue", "issues":
return gitea.IssueTypeIssue, nil
case "pull", "pulls", "pr":
return gitea.IssueTypePull, nil
default:
return "", errors.New("unknown kind '" + kindStr + "'")
}
}

View File

@@ -78,8 +78,8 @@ func TestPaginationFlags(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
}) })
} }
} }
func TestPaginationFailures(t *testing.T) { func TestPaginationFailures(t *testing.T) {
cases := []struct { cases := []struct {
name string name string

View File

@@ -5,7 +5,6 @@ package issues
import ( import (
stdctx "context" stdctx "context"
"errors"
"fmt" "fmt"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
@@ -35,7 +34,7 @@ func editIssueState(_ stdctx.Context, cmd *cli.Command, opts gitea.EditIssueOpti
ctx := context.InitCommand(cmd) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.Args().Len() == 0 { if ctx.Args().Len() == 0 {
return errors.New(ctx.Command.ArgsUsage) return fmt.Errorf("missing required argument: %s", ctx.Command.ArgsUsage)
} }
indices, err := utils.ArgsToIndices(ctx.Args().Slice()) indices, err := utils.ArgsToIndices(ctx.Args().Slice())

View File

@@ -29,7 +29,7 @@ func runIssuesCreate(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.NumFlags() == 0 { if ctx.IsInteractiveMode() {
err := interact.CreateIssue(ctx.Login, ctx.Owner, ctx.Repo) err := interact.CreateIssue(ctx.Login, ctx.Owner, ctx.Repo)
if err != nil && !interact.IsQuitting(err) { if err != nil && !interact.IsQuitting(err) {
return err return err

View File

@@ -49,7 +49,7 @@ func runIssuesEdit(_ stdctx.Context, cmd *cli.Command) error {
client := ctx.Login.Client() client := ctx.Login.Client()
for _, opts.Index = range indices { for _, opts.Index = range indices {
if ctx.NumFlags() == 0 { if ctx.IsInteractiveMode() {
var err error var err error
opts, err = interact.EditIssue(*ctx, opts.Index) opts, err = interact.EditIssue(*ctx, opts.Index)
if err != nil { if err != nil {

View File

@@ -5,7 +5,6 @@ package issues
import ( import (
stdctx "context" stdctx "context"
"fmt"
"time" "time"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
@@ -36,31 +35,16 @@ var CmdIssuesList = cli.Command{
func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error { func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx := context.InitCommand(cmd)
state := gitea.StateOpen state, err := flags.ParseState(ctx.String("state"))
switch ctx.String("state") { if err != nil {
case "all": return err
state = gitea.StateAll
case "", "open":
state = gitea.StateOpen
case "closed":
state = gitea.StateClosed
default:
return fmt.Errorf("unknown state '%s'", ctx.String("state"))
} }
kind := gitea.IssueTypeIssue kind, err := flags.ParseIssueKind(ctx.String("kind"), gitea.IssueTypeIssue)
switch ctx.String("kind") { if err != nil {
case "", "issues", "issue": return err
kind = gitea.IssueTypeIssue
case "pulls", "pull", "pr":
kind = gitea.IssueTypePull
case "all":
kind = gitea.IssueTypeAll
default:
return fmt.Errorf("unknown kind '%s'", ctx.String("kind"))
} }
var err error
var from, until time.Time var from, until time.Time
if ctx.IsSet("from") { if ctx.IsSet("from") {
from, err = dateparse.ParseLocal(ctx.String("from")) from, err = dateparse.ParseLocal(ctx.String("from"))
@@ -97,7 +81,6 @@ func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error {
Since: from, Since: from,
Before: until, Before: until,
}) })
if err != nil { if err != nil {
return err return err
} }
@@ -116,7 +99,6 @@ func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error {
Before: until, Before: until,
Owner: owner, Owner: owner,
}) })
if err != nil { if err != nil {
return err return err
} }

View File

@@ -20,7 +20,7 @@ var CmdIssuesReopen = cli.Command{
Description: `Change state of one or more issues to 'open'`, Description: `Change state of one or more issues to 'open'`,
ArgsUsage: "<issue index> [<issue index>...]", ArgsUsage: "<issue index> [<issue index>...]",
Action: func(ctx context.Context, cmd *cli.Command) error { Action: func(ctx context.Context, cmd *cli.Command) error {
var s = gitea.StateOpen s := gitea.StateOpen
return editIssueState(ctx, cmd, gitea.EditIssueOption{State: &s}) return editIssueState(ctx, cmd, gitea.EditIssueOption{State: &s})
}, },
Flags: flags.AllDefaultFlags, Flags: flags.AllDefaultFlags,

View File

@@ -26,7 +26,7 @@ const (
) )
func createTestIssue(comments int, isClosed bool) gitea.Issue { func createTestIssue(comments int, isClosed bool) gitea.Issue {
var issue = gitea.Issue{ issue := gitea.Issue{
ID: 42, ID: 42,
Index: 1, Index: 1,
Title: "Test issue", Title: "Test issue",
@@ -59,7 +59,7 @@ func createTestIssue(comments int, isClosed bool) gitea.Issue {
} }
if isClosed { if isClosed {
var closed = time.Date(2025, 11, 10, 21, 20, 19, 0, time.UTC) closed := time.Date(2025, 11, 10, 21, 20, 19, 0, time.UTC)
issue.Closed = &closed issue.Closed = &closed
} }
@@ -70,7 +70,6 @@ func createTestIssue(comments int, isClosed bool) gitea.Issue {
} }
return issue return issue
} }
func createTestIssueComments(comments int) []gitea.Comment { func createTestIssueComments(comments int) []gitea.Comment {
@@ -90,7 +89,6 @@ func createTestIssueComments(comments int) []gitea.Comment {
} }
return result return result
} }
func TestRunIssueDetailAsJSON(t *testing.T) { func TestRunIssueDetailAsJSON(t *testing.T) {
@@ -99,9 +97,6 @@ func TestRunIssueDetailAsJSON(t *testing.T) {
issue gitea.Issue issue gitea.Issue
comments []gitea.Comment comments []gitea.Comment
flagComments bool flagComments bool
flagOutput string
flagOut string
closed bool
} }
cmd := cli.Command{ cmd := cli.Command{
@@ -266,5 +261,4 @@ func TestRunIssueDetailAsJSON(t *testing.T) {
assert.Equal(t, expected, actual, "Expected structs differ from expected one") assert.Equal(t, expected, actual, "Expected structs differ from expected one")
}) })
} }
} }

View File

@@ -50,14 +50,15 @@ func runLabelCreate(_ stdctx.Context, cmd *cli.Command) error {
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
labelFile := ctx.String("file") labelFile := ctx.String("file")
var err error
if len(labelFile) == 0 { if len(labelFile) == 0 {
_, _, err = ctx.Login.Client().CreateLabel(ctx.Owner, ctx.Repo, gitea.CreateLabelOption{ _, _, err := ctx.Login.Client().CreateLabel(ctx.Owner, ctx.Repo, gitea.CreateLabelOption{
Name: ctx.String("name"), Name: ctx.String("name"),
Color: ctx.String("color"), Color: ctx.String("color"),
Description: ctx.String("description"), Description: ctx.String("description"),
}) })
} else { return err
}
f, err := os.Open(labelFile) f, err := os.Open(labelFile)
if err != nil { if err != nil {
return err return err
@@ -65,7 +66,7 @@ func runLabelCreate(_ stdctx.Context, cmd *cli.Command) error {
defer f.Close() defer f.Close()
scanner := bufio.NewScanner(f) scanner := bufio.NewScanner(f)
var i = 1 i := 1
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
color, name, description := splitLabelLine(line) color, name, description := splitLabelLine(line)
@@ -77,13 +78,15 @@ func runLabelCreate(_ stdctx.Context, cmd *cli.Command) error {
Color: color, Color: color,
Description: description, Description: description,
}) })
if err != nil {
return err
}
} }
i++ i++
} }
}
return err return nil
} }
func splitLabelLine(line string) (string, string, string) { func splitLabelLine(line string) (string, string, string) {

View File

@@ -20,7 +20,7 @@ func TestParseLabelLine(t *testing.T) {
` `
scanner := bufio.NewScanner(strings.NewReader(labels)) scanner := bufio.NewScanner(strings.NewReader(labels))
var i = 1 i := 1
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
color, name, description := splitLabelLine(line) color, name, description := splitLabelLine(line)

View File

@@ -67,7 +67,6 @@ func runLabelUpdate(_ stdctx.Context, cmd *cli.Command) error {
Color: pColor, Color: pColor,
Description: pDescription, Description: pDescription,
}) })
if err != nil { if err != nil {
return err return err
} }

View File

@@ -67,7 +67,7 @@ func runMilestonesCreate(_ stdctx.Context, cmd *cli.Command) error {
state = gitea.StateClosed state = gitea.StateClosed
} }
if ctx.NumFlags() == 0 { if ctx.IsInteractiveMode() {
if err := interact.CreateMilestone(ctx.Login, ctx.Owner, ctx.Repo); err != nil && !interact.IsQuitting(err) { if err := interact.CreateMilestone(ctx.Login, ctx.Owner, ctx.Repo); err != nil && !interact.IsQuitting(err) {
return err return err
} }

View File

@@ -75,29 +75,23 @@ func runMilestoneIssueList(_ stdctx.Context, cmd *cli.Command) error {
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
client := ctx.Login.Client() client := ctx.Login.Client()
state := gitea.StateOpen state, err := flags.ParseState(ctx.String("state"))
switch ctx.String("state") { if err != nil {
case "all": return err
state = gitea.StateAll
case "closed":
state = gitea.StateClosed
} }
kind := gitea.IssueTypeAll kind, err := flags.ParseIssueKind(ctx.String("kind"), gitea.IssueTypeAll)
switch ctx.String("kind") { if err != nil {
case "issue": return err
kind = gitea.IssueTypeIssue
case "pull":
kind = gitea.IssueTypePull
} }
if ctx.Args().Len() != 1 { if ctx.Args().Len() != 1 {
return fmt.Errorf("Must specify milestone name") return fmt.Errorf("milestone name is required")
} }
milestone := ctx.Args().First() milestone := ctx.Args().First()
// make sure milestone exist // make sure milestone exist
_, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, milestone) _, _, err = client.GetMilestoneByName(ctx.Owner, ctx.Repo, milestone)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -48,15 +48,12 @@ func RunMilestonesList(_ stdctx.Context, cmd *cli.Command) error {
return err return err
} }
state := gitea.StateOpen state, err := flags.ParseState(ctx.String("state"))
switch ctx.String("state") { if err != nil {
case "all": return err
state = gitea.StateAll
if !cmd.IsSet("fields") { // add to default fields
fields = append(fields, "state")
} }
case "closed": if state == gitea.StateAll && !cmd.IsSet("fields") {
state = gitea.StateClosed fields = append(fields, "state")
} }
client := ctx.Login.Client() client := ctx.Login.Client()
@@ -64,7 +61,6 @@ func RunMilestonesList(_ stdctx.Context, cmd *cli.Command) error {
ListOptions: flags.GetListOptions(), ListOptions: flags.GetListOptions(),
State: state, State: state,
}) })
if err != nil { if err != nil {
return err return err
} }

View File

@@ -5,7 +5,6 @@ package milestones
import ( import (
stdctx "context" stdctx "context"
"errors"
"fmt" "fmt"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
@@ -33,7 +32,7 @@ func editMilestoneStatus(_ stdctx.Context, cmd *cli.Command, close bool) error {
ctx := context.InitCommand(cmd) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.Args().Len() == 0 { if ctx.Args().Len() == 0 {
return errors.New(ctx.Command.ArgsUsage) return fmt.Errorf("missing required argument: %s", ctx.Command.ArgsUsage)
} }
state := gitea.StateOpen state := gitea.StateOpen

View File

@@ -56,7 +56,7 @@ func RunOrganizationCreate(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx := context.InitCommand(cmd)
if ctx.Args().Len() < 1 { if ctx.Args().Len() < 1 {
return fmt.Errorf("You have to specify the organization name you want to create") return fmt.Errorf("organization name is required")
} }
var visibility gitea.VisibleType var visibility gitea.VisibleType

View File

@@ -33,7 +33,7 @@ func RunOrganizationDelete(_ stdctx.Context, cmd *cli.Command) error {
client := ctx.Login.Client() client := ctx.Login.Client()
if ctx.Args().Len() < 1 { if ctx.Args().Len() < 1 {
return fmt.Errorf("You have to specify the organization name you want to delete") return fmt.Errorf("organization name is required")
} }
response, err := client.DeleteOrg(ctx.Args().First()) response, err := client.DeleteOrg(ctx.Args().First())

View File

@@ -12,7 +12,6 @@ import (
"code.gitea.io/tea/modules/interact" "code.gitea.io/tea/modules/interact"
"code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils" "code.gitea.io/tea/modules/utils"
"code.gitea.io/tea/modules/workaround"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
@@ -67,9 +66,6 @@ func runPullDetail(_ stdctx.Context, cmd *cli.Command, index string) error {
if err != nil { if err != nil {
return err return err
} }
if err := workaround.FixPullHeadSha(client, pr); err != nil {
return err
}
reviews, _, err := client.ListPullReviews(ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{ reviews, _, err := client.ListPullReviews(ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{
ListOptions: gitea.ListOptions{Page: -1}, ListOptions: gitea.ListOptions{Page: -1},

View File

@@ -4,16 +4,11 @@
package pulls package pulls
import ( import (
"fmt"
"strings"
stdctx "context" stdctx "context"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context" "code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@@ -26,20 +21,7 @@ var CmdPullsApprove = cli.Command{
ArgsUsage: "<pull index> [<comment>]", ArgsUsage: "<pull index> [<comment>]",
Action: func(_ stdctx.Context, cmd *cli.Command) error { Action: func(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) return runPullReview(ctx, gitea.ReviewStateApproved, false)
if ctx.Args().Len() == 0 {
return fmt.Errorf("Must specify a PR index")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
comment := strings.Join(ctx.Args().Tail(), " ")
return task.CreatePullReview(ctx, idx, gitea.ReviewStateApproved, comment, nil)
}, },
Flags: flags.AllDefaultFlags, Flags: flags.AllDefaultFlags,
} }

View File

@@ -40,7 +40,7 @@ func runPullsCheckout(_ stdctx.Context, cmd *cli.Command) error {
RemoteRepo: true, RemoteRepo: true,
}) })
if ctx.Args().Len() != 1 { if ctx.Args().Len() != 1 {
return fmt.Errorf("Must specify a PR index") return fmt.Errorf("pull request index is required")
} }
idx, err := utils.ArgToIndex(ctx.Args().First()) idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil { if err != nil {

View File

@@ -35,7 +35,7 @@ func runPullsClean(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{LocalRepo: true}) ctx.Ensure(context.CtxRequirement{LocalRepo: true})
if ctx.Args().Len() != 1 { if ctx.Args().Len() != 1 {
return fmt.Errorf("Must specify a PR index") return fmt.Errorf("pull request index is required")
} }
idx, err := utils.ArgToIndex(ctx.Args().First()) idx, err := utils.ArgToIndex(ctx.Args().First())

View File

@@ -19,7 +19,7 @@ var CmdPullsClose = cli.Command{
Description: `Change state of one or more pull requests to 'closed'`, Description: `Change state of one or more pull requests to 'closed'`,
ArgsUsage: "<pull index> [<pull index>...]", ArgsUsage: "<pull index> [<pull index>...]",
Action: func(ctx context.Context, cmd *cli.Command) error { Action: func(ctx context.Context, cmd *cli.Command) error {
var s = gitea.StateClosed s := gitea.StateClosed
return editPullState(ctx, cmd, gitea.EditPullRequestOption{State: &s}) return editPullState(ctx, cmd, gitea.EditPullRequestOption{State: &s})
}, },
Flags: flags.AllDefaultFlags, Flags: flags.AllDefaultFlags,

View File

@@ -48,7 +48,7 @@ func runPullsCreate(_ stdctx.Context, cmd *cli.Command) error {
}) })
// no args -> interactive mode // no args -> interactive mode
if ctx.NumFlags() == 0 { if ctx.IsInteractiveMode() {
if err := interact.CreatePull(ctx); err != nil && !interact.IsQuitting(err) { if err := interact.CreatePull(ctx); err != nil && !interact.IsQuitting(err) {
return err return err
} }

View File

@@ -20,7 +20,7 @@ func editPullState(_ stdctx.Context, cmd *cli.Command, opts gitea.EditPullReques
ctx := context.InitCommand(cmd) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.Args().Len() == 0 { if ctx.Args().Len() == 0 {
return fmt.Errorf("Please provide a Pull Request index") return fmt.Errorf("pull request index is required")
} }
indices, err := utils.ArgsToIndices(ctx.Args().Slice()) indices, err := utils.ArgsToIndices(ctx.Args().Slice())

View File

@@ -33,21 +33,15 @@ func RunPullsList(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
state := gitea.StateOpen state, err := flags.ParseState(ctx.String("state"))
switch ctx.String("state") { if err != nil {
case "all": return err
state = gitea.StateAll
case "open":
state = gitea.StateOpen
case "closed":
state = gitea.StateClosed
} }
prs, _, err := ctx.Login.Client().ListRepoPullRequests(ctx.Owner, ctx.Repo, gitea.ListPullRequestsOptions{ prs, _, err := ctx.Login.Client().ListRepoPullRequests(ctx.Owner, ctx.Repo, gitea.ListPullRequestsOptions{
ListOptions: flags.GetListOptions(), ListOptions: flags.GetListOptions(),
State: state, State: state,
}) })
if err != nil { if err != nil {
return err return err
} }

View File

@@ -5,15 +5,10 @@ package pulls
import ( import (
stdctx "context" stdctx "context"
"fmt"
"strings"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/utils"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/context"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@@ -25,20 +20,7 @@ var CmdPullsReject = cli.Command{
ArgsUsage: "<pull index> <reason>", ArgsUsage: "<pull index> <reason>",
Action: func(_ stdctx.Context, cmd *cli.Command) error { Action: func(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd) ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) return runPullReview(ctx, gitea.ReviewStateRequestChanges, true)
if ctx.Args().Len() < 2 {
return fmt.Errorf("Must specify a PR index and comment")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
comment := strings.Join(ctx.Args().Tail(), " ")
return task.CreatePullReview(ctx, idx, gitea.ReviewStateRequestChanges, comment, nil)
}, },
Flags: flags.AllDefaultFlags, Flags: flags.AllDefaultFlags,
} }

View File

@@ -20,7 +20,7 @@ var CmdPullsReopen = cli.Command{
Description: `Change state of one or more pull requests to 'open'`, Description: `Change state of one or more pull requests to 'open'`,
ArgsUsage: "<pull index> [<pull index>...]", ArgsUsage: "<pull index> [<pull index>...]",
Action: func(ctx context.Context, cmd *cli.Command) error { Action: func(ctx context.Context, cmd *cli.Command) error {
var s = gitea.StateOpen s := gitea.StateOpen
return editPullState(ctx, cmd, gitea.EditPullRequestOption{State: &s}) return editPullState(ctx, cmd, gitea.EditPullRequestOption{State: &s})
}, },
Flags: flags.AllDefaultFlags, Flags: flags.AllDefaultFlags,

View File

@@ -0,0 +1,40 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package pulls
import (
"fmt"
"strings"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/task"
"code.gitea.io/tea/modules/utils"
)
// runPullReview handles the common logic for approving/rejecting pull requests
func runPullReview(ctx *context.TeaContext, state gitea.ReviewStateType, requireComment bool) error {
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
minArgs := 1
if requireComment {
minArgs = 2
}
if ctx.Args().Len() < minArgs {
if requireComment {
return fmt.Errorf("pull request index and comment are required")
}
return fmt.Errorf("pull request index is required")
}
idx, err := utils.ArgToIndex(ctx.Args().First())
if err != nil {
return err
}
comment := strings.Join(ctx.Args().Tail(), " ")
return task.CreatePullReview(ctx, idx, state, comment, nil)
}

View File

@@ -68,17 +68,24 @@ func RunReposList(_ stdctx.Context, cmd *cli.Command) error {
ListOptions: flags.GetListOptions(), ListOptions: flags.GetListOptions(),
StarredByUserID: user.ID, StarredByUserID: user.ID,
}) })
if err != nil {
return err
}
} else if teaCmd.Bool("watched") { } else if teaCmd.Bool("watched") {
var err error
rps, _, err = client.GetMyWatchedRepos() // TODO: this does not expose pagination.. rps, _, err = client.GetMyWatchedRepos() // TODO: this does not expose pagination..
if err != nil {
return err
}
} else { } else {
var err error
rps, _, err = client.ListMyRepos(gitea.ListReposOptions{ rps, _, err = client.ListMyRepos(gitea.ListReposOptions{
ListOptions: flags.GetListOptions(), ListOptions: flags.GetListOptions(),
}) })
}
if err != nil { if err != nil {
return err return err
} }
}
reposFiltered := rps reposFiltered := rps
if typeFilter != gitea.RepoTypeNone { if typeFilter != gitea.RepoTypeNone {

View File

@@ -157,7 +157,6 @@ func runRepoMigrate(_ stdctx.Context, cmd *cli.Command) error {
} }
repo, _, err = client.MigrateRepo(opts) repo, _, err = client.MigrateRepo(opts)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -62,7 +62,7 @@ func runReposSearch(_ stdctx.Context, cmd *cli.Command) error {
var ownerID int64 var ownerID int64
if teaCmd.IsSet("owner") { if teaCmd.IsSet("owner") {
// test if owner is a organisation // test if owner is an organization
org, _, err := client.GetOrg(teaCmd.String("owner")) org, _, err := client.GetOrg(teaCmd.String("owner"))
if err != nil { if err != nil {
// HACK: the client does not return a response on 404, so we can't check res.StatusCode // HACK: the client does not return a response on 404, so we can't check res.StatusCode

View File

@@ -65,6 +65,8 @@ Depending on your permissions on the repository, only your own tracked times mig
Usage: "Show all times tracked by you across all repositories (overrides command arguments)", Usage: "Show all times tracked by you across all repositories (overrides command arguments)",
}, },
timeFieldsFlag, timeFieldsFlag,
&flags.PaginationPageFlag,
&flags.PaginationLimitFlag,
}, flags.AllDefaultFlags...), }, flags.AllDefaultFlags...),
} }
@@ -92,11 +94,15 @@ func RunTimesList(_ stdctx.Context, cmd *cli.Command) error {
} }
} }
opts := gitea.ListTrackedTimesOptions{Since: from, Before: until} opts := gitea.ListTrackedTimesOptions{
ListOptions: flags.GetListOptions(),
Since: from,
Before: until,
}
user := ctx.Args().First() user := ctx.Args().First()
if ctx.Bool("mine") { if ctx.Bool("mine") {
times, _, err = client.GetMyTrackedTimes() times, _, err = client.ListMyTrackedTimes(opts)
fields = []string{"created", "repo", "issue", "duration"} fields = []string{"created", "repo", "issue", "duration"}
} else if user == "" { } else if user == "" {
// get all tracked times on the repo // get all tracked times on the repo
@@ -104,9 +110,9 @@ func RunTimesList(_ stdctx.Context, cmd *cli.Command) error {
fields = []string{"created", "issue", "user", "duration"} fields = []string{"created", "issue", "user", "duration"}
} else if strings.HasPrefix(user, "#") { } else if strings.HasPrefix(user, "#") {
// get all tracked times on the specified issue // get all tracked times on the specified issue
issue, err := utils.ArgToIndex(user) issue, parseErr := utils.ArgToIndex(user)
if err != nil { if parseErr != nil {
return err return parseErr
} }
times, _, err = client.ListIssueTrackedTimes(ctx.Owner, ctx.Repo, issue, opts) times, _, err = client.ListIssueTrackedTimes(ctx.Owner, ctx.Repo, issue, opts)
fields = []string{"created", "user", "duration"} fields = []string{"created", "user", "duration"}

View File

@@ -63,7 +63,7 @@ func runWebhooksDelete(ctx stdctx.Context, cmd *cli.Command) error {
var response string var response string
fmt.Scanln(&response) fmt.Scanln(&response)
if response != "y" && response != "Y" && response != "yes" { if response != "y" && response != "Y" && response != "yes" {
fmt.Println("Deletion cancelled.") fmt.Println("Deletion canceled.")
return nil return nil
} }
} }

View File

@@ -435,9 +435,9 @@ func TestDeleteSuccessMessage(t *testing.T) {
} }
func TestDeleteCancellationMessage(t *testing.T) { func TestDeleteCancellationMessage(t *testing.T) {
expectedMessage := "Deletion cancelled." expectedMessage := "Deletion canceled."
assert.NotEmpty(t, expectedMessage) assert.NotEmpty(t, expectedMessage)
assert.Contains(t, expectedMessage, "cancelled") assert.Contains(t, expectedMessage, "canceled")
assert.NotContains(t, expectedMessage, "\n", "Cancellation message should not end with newline") assert.NotContains(t, expectedMessage, "\n", "Cancellation message should not end with newline")
} }

View File

@@ -849,12 +849,16 @@ Operate on tracked times of a repository's issues & pulls
**--from, -f**="": Show only times tracked after this date **--from, -f**="": Show only times tracked after this date
**--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--mine, -m**: Show all times tracked by you across all repositories (overrides command arguments) **--mine, -m**: Show all times tracked by you across all repositories (overrides command arguments)
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional **--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
@@ -909,12 +913,16 @@ List tracked times on issues & pulls
**--from, -f**="": Show only times tracked after this date **--from, -f**="": Show only times tracked after this date
**--limit, --lm**="": specify limit of items per page (default: 30)
**--login, -l**="": Use a different Gitea Login. Optional **--login, -l**="": Use a different Gitea Login. Optional
**--mine, -m**: Show all times tracked by you across all repositories (overrides command arguments) **--mine, -m**: Show all times tracked by you across all repositories (overrides command arguments)
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
**--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional **--remote, -R**="": Discover Gitea login from remote. Optional
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional **--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional

View File

@@ -83,14 +83,15 @@ func loadConfig() (err error) {
ymlPath := GetConfigPath() ymlPath := GetConfigPath()
exist, _ := utils.FileExist(ymlPath) exist, _ := utils.FileExist(ymlPath)
if exist { if exist {
bs, err := os.ReadFile(ymlPath) bs, readErr := os.ReadFile(ymlPath)
if err != nil { if readErr != nil {
err = fmt.Errorf("Failed to read config file: %s", ymlPath) err = fmt.Errorf("failed to read config file %s: %w", ymlPath, readErr)
return
} }
err = yaml.Unmarshal(bs, &config) if unmarshalErr := yaml.Unmarshal(bs, &config); unmarshalErr != nil {
if err != nil { err = fmt.Errorf("failed to parse config file %s: %w", ymlPath, unmarshalErr)
err = fmt.Errorf("Failed to parse contents of config file: %s", ymlPath) return
} }
} }
}) })

View File

@@ -46,6 +46,12 @@ func (ctx *TeaContext) GetRemoteRepoHTMLURL() string {
return path.Join(ctx.Login.URL, ctx.Owner, ctx.Repo) return path.Join(ctx.Login.URL, ctx.Owner, ctx.Repo)
} }
// IsInteractiveMode returns true if the command is running in interactive mode
// (no flags provided and stdout is a terminal)
func (ctx *TeaContext) IsInteractiveMode() bool {
return ctx.Command.NumFlags() == 0
}
// Ensure checks if requirements on the context are set, and terminates otherwise. // Ensure checks if requirements on the context are set, and terminates otherwise.
func (ctx *TeaContext) Ensure(req CtxRequirement) { func (ctx *TeaContext) Ensure(req CtxRequirement) {
if req.LocalRepo && ctx.LocalRepo == nil { if req.LocalRepo && ctx.LocalRepo == nil {

View File

@@ -143,7 +143,7 @@ func (r TeaRepo) TeaFindBranchByName(branchName, repoURL string) (b *git_config.
defer iter.Close() defer iter.Close()
var remoteRefName git_plumbing.ReferenceName var remoteRefName git_plumbing.ReferenceName
var localRefName git_plumbing.ReferenceName var localRefName git_plumbing.ReferenceName
var remoteSearchingName = fmt.Sprintf("%s/%s", remoteName, branchName) remoteSearchingName := fmt.Sprintf("%s/%s", remoteName, branchName)
err = iter.ForEach(func(ref *git_plumbing.Reference) error { err = iter.ForEach(func(ref *git_plumbing.Reference) error {
if ref.Name().IsRemote() && ref.Name().Short() == remoteSearchingName { if ref.Name().IsRemote() && ref.Name().Short() == remoteSearchingName {
remoteRefName = ref.Name() remoteRefName = ref.Name()

View File

@@ -37,7 +37,7 @@ func TestRepoFromPath_Worktree(t *testing.T) {
// Create an initial commit (required for worktree) // Create an initial commit (required for worktree)
readmePath := filepath.Join(mainRepoPath, "README.md") readmePath := filepath.Join(mainRepoPath, "README.md")
err = os.WriteFile(readmePath, []byte("# Test Repo\n"), 0644) err = os.WriteFile(readmePath, []byte("# Test Repo\n"), 0o644)
assert.NoError(t, err) assert.NoError(t, err)
cmd = exec.Command("git", "-C", mainRepoPath, "add", "README.md") cmd = exec.Command("git", "-C", mainRepoPath, "add", "README.md")
assert.NoError(t, cmd.Run()) assert.NoError(t, cmd.Run())

View File

@@ -19,7 +19,7 @@ import (
// MergePull interactively creates a PR // MergePull interactively creates a PR
func MergePull(ctx *context.TeaContext) error { func MergePull(ctx *context.TeaContext) error {
if ctx.LocalRepo == nil { if ctx.LocalRepo == nil {
return fmt.Errorf("Must specify a PR index") return fmt.Errorf("pull request index is required")
} }
branch, _, err := ctx.LocalRepo.TeaGetCurrentBranchNameAndSHA() branch, _, err := ctx.LocalRepo.TeaGetCurrentBranchNameAndSHA()
@@ -51,9 +51,12 @@ func getPullIndex(ctx *context.TeaContext, branch string) (int64, error) {
// paginated fetch // paginated fetch
var prs []*gitea.PullRequest var prs []*gitea.PullRequest
var err error
for { for {
var err error
prs, _, err = c.ListRepoPullRequests(ctx.Owner, ctx.Repo, opts) prs, _, err = c.ListRepoPullRequests(ctx.Owner, ctx.Repo, opts)
if err != nil {
return 0, err
}
if len(prs) == 0 { if len(prs) == 0 {
return 0, fmt.Errorf("No open PRs found") return 0, fmt.Errorf("No open PRs found")
} }

View File

@@ -12,7 +12,7 @@ import (
// NotificationsList prints a listing of notification threads // NotificationsList prints a listing of notification threads
func NotificationsList(news []*gitea.NotificationThread, output string, fields []string) { func NotificationsList(news []*gitea.NotificationThread, output string, fields []string) {
var printables = make([]printable, len(news)) printables := make([]printable, len(news))
for i, x := range news { for i, x := range news {
printables[i] = &printableNotification{x} printables[i] = &printableNotification{x}
} }

View File

@@ -111,7 +111,6 @@ func formatReviews(pr *gitea.PullRequest, reviews []*gitea.PullReview) string {
reviewByUserOrTeam[fmt.Sprintf("team_%d", review.ReviewerTeam.ID)] = review reviewByUserOrTeam[fmt.Sprintf("team_%d", review.ReviewerTeam.ID)] = review
} }
} }
} }
} }
@@ -173,7 +172,7 @@ var PullFields = []string{
func printPulls(pulls []*gitea.PullRequest, output string, fields []string) { func printPulls(pulls []*gitea.PullRequest, output string, fields []string) {
labelMap := map[int64]string{} labelMap := map[int64]string{}
var printables = make([]printable, len(pulls)) printables := make([]printable, len(pulls))
machineReadable := isMachineReadable(output) machineReadable := isMachineReadable(output)
for i, x := range pulls { for i, x := range pulls {
@@ -227,13 +226,13 @@ func (x printablePull) FormatField(field string, machineReadable bool) string {
} }
return "" return ""
case "labels": case "labels":
var labels = make([]string, len(x.Labels)) labels := make([]string, len(x.Labels))
for i, l := range x.Labels { for i, l := range x.Labels {
labels[i] = (*x.formattedLabels)[l.ID] labels[i] = (*x.formattedLabels)[l.ID]
} }
return strings.Join(labels, " ") return strings.Join(labels, " ")
case "assignees": case "assignees":
var assignees = make([]string, len(x.Assignees)) assignees := make([]string, len(x.Assignees))
for i, a := range x.Assignees { for i, a := range x.Assignees {
assignees[i] = formatUserName(a) assignees[i] = formatUserName(a)
} }

View File

@@ -11,7 +11,7 @@ import (
// TrackedTimesList print list of tracked times to stdout // TrackedTimesList print list of tracked times to stdout
func TrackedTimesList(times []*gitea.TrackedTime, outputType string, fields []string, printTotal bool) { func TrackedTimesList(times []*gitea.TrackedTime, outputType string, fields []string, printTotal bool) {
var printables = make([]printable, len(times)) printables := make([]printable, len(times))
var totalDuration int64 var totalDuration int64
for i, t := range times { for i, t := range times {
totalDuration += t.Time totalDuration += t.Time

View File

@@ -53,7 +53,7 @@ func UserDetails(user *gitea.User) {
// UserList prints a listing of the users // UserList prints a listing of the users
func UserList(user []*gitea.User, output string, fields []string) { func UserList(user []*gitea.User, output string, fields []string) {
var printables = make([]printable, len(user)) printables := make([]printable, len(user))
for i, u := range user { for i, u := range user {
printables[i] = &printableUser{u} printables[i] = &printableUser{u}
} }

View File

@@ -51,7 +51,7 @@ func CreateLogin(name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCe
// checks ... // checks ...
// ... if we have a url // ... if we have a url
if len(giteaURL) == 0 { if len(giteaURL) == 0 {
return fmt.Errorf("You have to input Gitea server URL") return fmt.Errorf("Gitea server URL is required")
} }
// ... if there already exist a login with same name // ... if there already exist a login with same name

View File

@@ -15,7 +15,6 @@ import (
// CreateMilestone creates a milestone in the given repo and prints the result // 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(login *config.Login, repoOwner, repoName, title, description string, deadline *time.Time, state gitea.StateType) error {
// title is required // title is required
if len(title) == 0 { if len(title) == 0 {
return fmt.Errorf("Title is required") return fmt.Errorf("Title is required")

View File

@@ -9,7 +9,6 @@ import (
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/config"
local_git "code.gitea.io/tea/modules/git" local_git "code.gitea.io/tea/modules/git"
"code.gitea.io/tea/modules/workaround"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
git_config "github.com/go-git/go-git/v5/config" git_config "github.com/go-git/go-git/v5/config"
@@ -29,9 +28,6 @@ func PullCheckout(
if err != nil { if err != nil {
return fmt.Errorf("couldn't fetch PR: %s", err) return fmt.Errorf("couldn't fetch PR: %s", err)
} }
if err := workaround.FixPullHeadSha(client, pr); err != nil {
return err
}
// FIXME: should use ctx.LocalRepo..? // FIXME: should use ctx.LocalRepo..?
localRepo, err := local_git.RepoForWorkdir() localRepo, err := local_git.RepoForWorkdir()

View File

@@ -8,7 +8,6 @@ import (
"code.gitea.io/tea/modules/config" "code.gitea.io/tea/modules/config"
local_git "code.gitea.io/tea/modules/git" local_git "code.gitea.io/tea/modules/git"
"code.gitea.io/tea/modules/workaround"
"code.gitea.io/sdk/gitea" "code.gitea.io/sdk/gitea"
git_config "github.com/go-git/go-git/v5/config" git_config "github.com/go-git/go-git/v5/config"
@@ -33,9 +32,6 @@ func PullClean(login *config.Login, repoOwner, repoName string, index int64, ign
if err != nil { if err != nil {
return err return err
} }
if err := workaround.FixPullHeadSha(client, pr); err != nil {
return err
}
if pr.State == gitea.StateOpen { if pr.State == gitea.StateOpen {
return fmt.Errorf("PR is still open, won't delete branches") return fmt.Errorf("PR is still open, won't delete branches")
@@ -96,15 +92,15 @@ call me again with the --ignore-sha flag`, remoteBranch)
if !remoteDeleted && pr.Head.Repository.Permissions.Push { if !remoteDeleted && pr.Head.Repository.Permissions.Push {
fmt.Printf("Deleting remote branch %s\n", remoteBranch) fmt.Printf("Deleting remote branch %s\n", remoteBranch)
url, err := r.TeaRemoteURL(branch.Remote) url, urlErr := r.TeaRemoteURL(branch.Remote)
if err != nil { if urlErr != nil {
return err return urlErr
} }
auth, err := local_git.GetAuthForURL(url, login.Token, login.SSHKey, callback) auth, authErr := local_git.GetAuthForURL(url, login.Token, login.SSHKey, callback)
if err != nil { if authErr != nil {
return err return authErr
} }
err = r.TeaDeleteRemoteBranch(branch.Remote, remoteBranch, auth) return r.TeaDeleteRemoteBranch(branch.Remote, remoteBranch, auth)
} }
return err return nil
} }

View File

@@ -40,7 +40,7 @@ func RepoClone(
return nil, err return nil, err
} }
// default path behaviour as native git // default path behavior as native git
if path == "" { if path == "" {
path = repoName path = repoName
} }

87
modules/utils/input.go Normal file
View File

@@ -0,0 +1,87 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package utils
import (
"bufio"
"fmt"
"io"
"os"
"strings"
"syscall"
"github.com/urfave/cli/v3"
"golang.org/x/term"
)
// ReadValueOptions contains options for reading a value from various sources
type ReadValueOptions struct {
// ResourceName is the name of the resource (e.g., "secret", "variable")
ResourceName string
// PromptMsg is the message to display when prompting interactively
PromptMsg string
// Hidden determines if the input should be hidden (for secrets/passwords)
Hidden bool
// AllowEmpty determines if empty values are allowed
AllowEmpty bool
}
// ReadValue reads a value from various sources in the following priority order:
// 1. From a file specified by --file flag
// 2. From stdin if --stdin flag is set
// 3. From command arguments (second argument)
// 4. Interactive prompt
func ReadValue(cmd *cli.Command, opts ReadValueOptions) (string, error) {
var value string
// 1. Read from file
if filePath := cmd.String("file"); filePath != "" {
content, err := os.ReadFile(filePath)
if err != nil {
return "", fmt.Errorf("failed to read file: %w", err)
}
value = strings.TrimSpace(string(content))
} else if cmd.Bool("stdin") {
// 2. Read from stdin
content, err := io.ReadAll(os.Stdin)
if err != nil {
return "", fmt.Errorf("failed to read from stdin: %w", err)
}
value = strings.TrimSpace(string(content))
} else if cmd.Args().Len() >= 2 {
// 3. Use provided argument
value = cmd.Args().Get(1)
} else {
// 4. Interactive prompt
if opts.PromptMsg == "" {
opts.PromptMsg = fmt.Sprintf("Enter %s value", opts.ResourceName)
}
fmt.Printf("%s: ", opts.PromptMsg)
if opts.Hidden {
// Hidden input for secrets/passwords
byteValue, err := term.ReadPassword(int(syscall.Stdin))
if err != nil {
return "", fmt.Errorf("failed to read %s value: %w", opts.ResourceName, err)
}
fmt.Println() // Add newline after hidden input
value = string(byteValue)
} else {
// Regular visible input - read entire line including spaces
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
return "", fmt.Errorf("failed to read %s value: %w", opts.ResourceName, err)
}
value = strings.TrimSpace(input)
}
}
// Validate non-empty if required
if !opts.AllowEmpty && value == "" {
return "", fmt.Errorf("%s value cannot be empty", opts.ResourceName)
}
return value, nil
}

View File

@@ -62,9 +62,9 @@ func AbsPathWithExpansion(p string) (string, error) {
} }
if p == "~" { if p == "~" {
return u.HomeDir, nil return u.HomeDir, nil
} else if strings.HasPrefix(p, "~/") { }
if strings.HasPrefix(p, "~/") {
return filepath.Join(u.HomeDir, p[2:]), nil return filepath.Join(u.HomeDir, p[2:]), nil
} else { }
return filepath.Abs(p) return filepath.Abs(p)
} }
}

View File

@@ -1,29 +0,0 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package workaround
import (
"fmt"
"code.gitea.io/sdk/gitea"
)
// FixPullHeadSha is a workaround for https://github.com/go-gitea/gitea/issues/12675
// When no head sha is available, this is because the branch got deleted in the base repo.
// pr.Head.Ref points in this case not to the head repo branch name, but the base repo ref,
// which stays available to resolve the commit sha.
func FixPullHeadSha(client *gitea.Client, pr *gitea.PullRequest) error {
owner := pr.Base.Repository.Owner.UserName
repo := pr.Base.Repository.Name
if pr.Head != nil && pr.Head.Sha == "" {
refs, _, err := client.GetRepoRefs(owner, repo, pr.Head.Ref)
if err != nil {
return err
} else if len(refs) == 0 {
return fmt.Errorf("unable to resolve PR ref '%s'", pr.Head.Ref)
}
pr.Head.Sha = refs[0].Object.SHA
}
return nil
}