diff --git a/.gitea/workflows/test-pr.yml b/.gitea/workflows/test-pr.yml index 105de7e..968d8b6 100644 --- a/.gitea/workflows/test-pr.yml +++ b/.gitea/workflows/test-pr.yml @@ -30,7 +30,6 @@ jobs: make vet make lint make fmt-check - make misspell-check make docs-check make build - run: curl --noproxy "*" http://gitea:3000/api/v1/version # verify connection to instance diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..8502078 --- /dev/null +++ b/.golangci.yml @@ -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 diff --git a/Makefile b/Makefile index dbb48c1..735b329 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,10 @@ SHASUM ?= shasum -a 256 export PATH := $($(GO) env GOPATH)/bin:$(PATH) 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),) VERSION ?= $(subst v,,$(DRONE_TAG)) @@ -49,7 +52,7 @@ clean: .PHONY: fmt fmt: - $(GOFMT) -w $(GOFILES) + $(GO) run $(GOFUMPT_PACKAGE) -w $(GOFILES) .PHONY: vet vet: @@ -60,21 +63,17 @@ vet: $(GO) vet -vettool=$(VET_TOOL) $(PACKAGES) .PHONY: lint -lint: install-lint-tools - $(GO) run github.com/mgechev/revive@v1.3.2 -config .revive.toml ./... || exit 1 +lint: + $(GO) run $(GOLANGCI_LINT_PACKAGE) run -.PHONY: misspell-check -misspell-check: install-lint-tools - $(GO) run github.com/client9/misspell/cmd/misspell@latest -error -i unknwon,destory $(GOFILES) - -.PHONY: misspell -misspell: install-lint-tools - $(GO) run github.com/client9/misspell/cmd/misspell@latest -w -i unknwon $(GOFILES) +.PHONY: lint-fix +lint-fix: + $(GO) run $(GOLANGCI_LINT_PACKAGE) run --fix .PHONY: fmt-check fmt-check: - # get all go files and run go fmt on them - @diff=$$($(GOFMT) -d $(GOFILES)); \ + # get all go files and run gofumpt on them + @diff=$$($(GO) run $(GOFUMPT_PACKAGE) -d $(GOFILES)); \ if [ -n "$$diff" ]; then \ echo "Please run 'make fmt' and commit the result:"; \ echo "$${diff}"; \ @@ -124,10 +123,3 @@ $(EXECUTABLE): $(SOURCES) build-image: 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 diff --git a/cmd/actions/secrets/create.go b/cmd/actions/secrets/create.go index d573398..b9b13a6 100644 --- a/cmd/actions/secrets/create.go +++ b/cmd/actions/secrets/create.go @@ -6,17 +6,13 @@ package secrets import ( stdctx "context" "fmt" - "io" - "os" - "strings" - "syscall" "code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/modules/context" + "code.gitea.io/tea/modules/utils" "code.gitea.io/sdk/gitea" "github.com/urfave/cli/v3" - "golang.org/x/term" ) // 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() secretName := cmd.Args().First() - var secretValue string - // Determine how to get the secret value - if cmd.String("file") != "" { - // Read from file - content, err := os.ReadFile(cmd.String("file")) - if err != nil { - return fmt.Errorf("failed to read file: %w", 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) + // Read secret value using the utility + secretValue, err := utils.ReadValue(cmd, utils.ReadValueOptions{ + ResourceName: "secret", + PromptMsg: fmt.Sprintf("Enter secret value for '%s'", secretName), + Hidden: true, + AllowEmpty: false, + }) + if err != nil { + return err } - if secretValue == "" { - return fmt.Errorf("secret value cannot be empty") - } - - _, err := client.CreateRepoActionSecret(c.Owner, c.Repo, gitea.CreateSecretOption{ + _, err = client.CreateRepoActionSecret(c.Owner, c.Repo, gitea.CreateSecretOption{ Name: secretName, Data: secretValue, }) diff --git a/cmd/actions/secrets/delete.go b/cmd/actions/secrets/delete.go index 255f24d..a031043 100644 --- a/cmd/actions/secrets/delete.go +++ b/cmd/actions/secrets/delete.go @@ -45,7 +45,7 @@ func runSecretsDelete(ctx stdctx.Context, cmd *cli.Command) error { var response string fmt.Scanln(&response) if response != "y" && response != "Y" && response != "yes" { - fmt.Println("Deletion cancelled.") + fmt.Println("Deletion canceled.") return nil } } diff --git a/cmd/actions/variables/delete.go b/cmd/actions/variables/delete.go index cf73fb1..b81ac64 100644 --- a/cmd/actions/variables/delete.go +++ b/cmd/actions/variables/delete.go @@ -45,7 +45,7 @@ func runVariablesDelete(ctx stdctx.Context, cmd *cli.Command) error { var response string fmt.Scanln(&response) if response != "y" && response != "Y" && response != "yes" { - fmt.Println("Deletion cancelled.") + fmt.Println("Deletion canceled.") return nil } } diff --git a/cmd/actions/variables/set.go b/cmd/actions/variables/set.go index 03a4cac..7a504c5 100644 --- a/cmd/actions/variables/set.go +++ b/cmd/actions/variables/set.go @@ -6,13 +6,11 @@ package variables import ( stdctx "context" "fmt" - "io" - "os" "regexp" - "strings" "code.gitea.io/tea/cmd/flags" "code.gitea.io/tea/modules/context" + "code.gitea.io/tea/modules/utils" "github.com/urfave/cli/v3" ) @@ -46,39 +44,26 @@ func runVariablesSet(ctx stdctx.Context, cmd *cli.Command) error { client := c.Login.Client() variableName := cmd.Args().First() - var variableValue string - - // Determine how to get the variable value - if cmd.String("file") != "" { - // Read from file - content, err := os.ReadFile(cmd.String("file")) - if err != nil { - return fmt.Errorf("failed to read file: %w", 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 err := validateVariableName(variableName); err != nil { + return err } - if variableValue == "" { - return fmt.Errorf("variable value cannot be empty") + // Read variable value using the utility + variableValue, err := utils.ReadValue(cmd, utils.ReadValueOptions{ + ResourceName: "variable", + PromptMsg: fmt.Sprintf("Enter variable value for '%s'", variableName), + Hidden: false, + AllowEmpty: false, + }) + if err != nil { + return err } - _, err := client.CreateRepoActionVariable(c.Owner, c.Repo, variableName, variableValue) + if err := validateVariableValue(variableValue); err != nil { + return err + } + + _, err = client.CreateRepoActionVariable(c.Owner, c.Repo, variableName, variableValue) if err != nil { return err } diff --git a/cmd/attachments/delete.go b/cmd/attachments/delete.go index 9abad42..238d92b 100644 --- a/cmd/attachments/delete.go +++ b/cmd/attachments/delete.go @@ -81,21 +81,3 @@ func runReleaseAttachmentDelete(_ stdctx.Context, cmd *cli.Command) error { 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") -} diff --git a/cmd/branches/list.go b/cmd/branches/list.go index ce47330..098b5f8 100644 --- a/cmd/branches/list.go +++ b/cmd/branches/list.go @@ -52,7 +52,6 @@ func RunBranchesList(_ stdctx.Context, cmd *cli.Command) error { branches, _, err = ctx.Login.Client().ListRepoBranches(owner, ctx.Repo, gitea.ListRepoBranchesOptions{ ListOptions: flags.GetListOptions(), }) - if err != nil { 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{ ListOptions: flags.GetListOptions(), }) - if err != nil { return err } diff --git a/cmd/clone.go b/cmd/clone.go index 7acb771..2bf24d4 100644 --- a/cmd/clone.go +++ b/cmd/clone.go @@ -58,7 +58,7 @@ func runRepoClone(ctx stdctx.Context, cmd *cli.Command) error { var ( login *config.Login = teaCmd.Login - owner string = teaCmd.Login.User + owner string repo string ) diff --git a/cmd/flags/generic.go b/cmd/flags/generic.go index f942a70..af2692a 100644 --- a/cmd/flags/generic.go +++ b/cmd/flags/generic.go @@ -141,3 +141,34 @@ var NotificationStateFlag = NewCsvFlag( func FieldsFlag(availableFields, defaultFields []string) *CsvFlag { 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 + "'") + } +} diff --git a/cmd/flags/generic_test.go b/cmd/flags/generic_test.go index 9f290b5..184afbf 100644 --- a/cmd/flags/generic_test.go +++ b/cmd/flags/generic_test.go @@ -55,7 +55,7 @@ func TestPaginationFlags(t *testing.T) { expectedPage: 2, expectedLimit: 20, }, - { //TODO: Should no paging be applied as -1 or a separate flag? It's not obvious that page=-1 turns off paging and limit is ignored + { // TODO: Should no paging be applied as -1 or a separate flag? It's not obvious that page=-1 turns off paging and limit is ignored name: "no paging", args: []string{"test", "--limit", "20", "--page", "-1"}, expectedPage: -1, @@ -78,8 +78,8 @@ func TestPaginationFlags(t *testing.T) { require.NoError(t, err) }) } - } + func TestPaginationFailures(t *testing.T) { cases := []struct { name string @@ -102,7 +102,7 @@ func TestPaginationFailures(t *testing.T) { expectedError: ErrPage, }, { - //urfave does not validate all flags in one pass + // urfave does not validate all flags in one pass name: "negative paging and paging", args: []string{"test", "--page", "-2", "--limit", "-10"}, expectedError: ErrPage, diff --git a/cmd/issues/close.go b/cmd/issues/close.go index 42caaf6..d70c25a 100644 --- a/cmd/issues/close.go +++ b/cmd/issues/close.go @@ -5,7 +5,6 @@ package issues import ( stdctx "context" - "errors" "fmt" "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.Ensure(context.CtxRequirement{RemoteRepo: true}) 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()) diff --git a/cmd/issues/create.go b/cmd/issues/create.go index 84593d3..2eb2699 100644 --- a/cmd/issues/create.go +++ b/cmd/issues/create.go @@ -29,7 +29,7 @@ func runIssuesCreate(_ stdctx.Context, cmd *cli.Command) error { ctx := context.InitCommand(cmd) ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) - if ctx.NumFlags() == 0 { + if ctx.IsInteractiveMode() { err := interact.CreateIssue(ctx.Login, ctx.Owner, ctx.Repo) if err != nil && !interact.IsQuitting(err) { return err diff --git a/cmd/issues/edit.go b/cmd/issues/edit.go index 2e49437..a673e35 100644 --- a/cmd/issues/edit.go +++ b/cmd/issues/edit.go @@ -49,7 +49,7 @@ func runIssuesEdit(_ stdctx.Context, cmd *cli.Command) error { client := ctx.Login.Client() for _, opts.Index = range indices { - if ctx.NumFlags() == 0 { + if ctx.IsInteractiveMode() { var err error opts, err = interact.EditIssue(*ctx, opts.Index) if err != nil { diff --git a/cmd/issues/list.go b/cmd/issues/list.go index 3bca2b4..dd1a376 100644 --- a/cmd/issues/list.go +++ b/cmd/issues/list.go @@ -5,7 +5,6 @@ package issues import ( stdctx "context" - "fmt" "time" "code.gitea.io/tea/cmd/flags" @@ -36,31 +35,16 @@ var CmdIssuesList = cli.Command{ func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error { ctx := context.InitCommand(cmd) - state := gitea.StateOpen - switch ctx.String("state") { - case "all": - state = gitea.StateAll - case "", "open": - state = gitea.StateOpen - case "closed": - state = gitea.StateClosed - default: - return fmt.Errorf("unknown state '%s'", ctx.String("state")) + state, err := flags.ParseState(ctx.String("state")) + if err != nil { + return err } - kind := gitea.IssueTypeIssue - switch ctx.String("kind") { - case "", "issues", "issue": - 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")) + kind, err := flags.ParseIssueKind(ctx.String("kind"), gitea.IssueTypeIssue) + if err != nil { + return err } - var err error var from, until time.Time if ctx.IsSet("from") { from, err = dateparse.ParseLocal(ctx.String("from")) @@ -97,7 +81,6 @@ func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error { Since: from, Before: until, }) - if err != nil { return err } @@ -116,7 +99,6 @@ func RunIssuesList(_ stdctx.Context, cmd *cli.Command) error { Before: until, Owner: owner, }) - if err != nil { return err } diff --git a/cmd/issues/reopen.go b/cmd/issues/reopen.go index df2fd6a..05e9c61 100644 --- a/cmd/issues/reopen.go +++ b/cmd/issues/reopen.go @@ -20,7 +20,7 @@ var CmdIssuesReopen = cli.Command{ Description: `Change state of one or more issues to 'open'`, ArgsUsage: " [...]", Action: func(ctx context.Context, cmd *cli.Command) error { - var s = gitea.StateOpen + s := gitea.StateOpen return editIssueState(ctx, cmd, gitea.EditIssueOption{State: &s}) }, Flags: flags.AllDefaultFlags, diff --git a/cmd/issues_test.go b/cmd/issues_test.go index 9aa162b..2785624 100644 --- a/cmd/issues_test.go +++ b/cmd/issues_test.go @@ -26,7 +26,7 @@ const ( ) func createTestIssue(comments int, isClosed bool) gitea.Issue { - var issue = gitea.Issue{ + issue := gitea.Issue{ ID: 42, Index: 1, Title: "Test issue", @@ -55,11 +55,11 @@ func createTestIssue(comments int, isClosed bool) gitea.Issue { {UserName: "testUser3"}, }, HTMLURL: "", - Closed: nil, //2025-11-10T21:20:19Z + Closed: nil, // 2025-11-10T21:20:19Z } 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 } @@ -70,7 +70,6 @@ func createTestIssue(comments int, isClosed bool) gitea.Issue { } return issue - } func createTestIssueComments(comments int) []gitea.Comment { @@ -90,7 +89,6 @@ func createTestIssueComments(comments int) []gitea.Comment { } return result - } func TestRunIssueDetailAsJSON(t *testing.T) { @@ -99,9 +97,6 @@ func TestRunIssueDetailAsJSON(t *testing.T) { issue gitea.Issue comments []gitea.Comment flagComments bool - flagOutput string - flagOut string - closed bool } cmd := cli.Command{ @@ -205,7 +200,7 @@ func TestRunIssueDetailAsJSON(t *testing.T) { require.NotEmpty(t, out, "Unexpected empty output from runIssueDetailAsJSON") - //setting expectations + // setting expectations var expectedLabels []labelData expectedLabels = []labelData{} @@ -266,5 +261,4 @@ func TestRunIssueDetailAsJSON(t *testing.T) { assert.Equal(t, expected, actual, "Expected structs differ from expected one") }) } - } diff --git a/cmd/labels/create.go b/cmd/labels/create.go index ec72b32..38e08d7 100644 --- a/cmd/labels/create.go +++ b/cmd/labels/create.go @@ -50,40 +50,43 @@ func runLabelCreate(_ stdctx.Context, cmd *cli.Command) error { ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) labelFile := ctx.String("file") - var err error 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"), Color: ctx.String("color"), Description: ctx.String("description"), }) - } else { - f, err := os.Open(labelFile) - if err != nil { - return err - } - defer f.Close() - - scanner := bufio.NewScanner(f) - var i = 1 - for scanner.Scan() { - line := scanner.Text() - color, name, description := splitLabelLine(line) - if color == "" || name == "" { - log.Printf("Line %d ignored because lack of enough fields: %s\n", i, line) - } else { - _, _, err = ctx.Login.Client().CreateLabel(ctx.Owner, ctx.Repo, gitea.CreateLabelOption{ - Name: name, - Color: color, - Description: description, - }) - } - - i++ - } + return err } - return err + f, err := os.Open(labelFile) + if err != nil { + return err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + i := 1 + for scanner.Scan() { + line := scanner.Text() + color, name, description := splitLabelLine(line) + if color == "" || name == "" { + log.Printf("Line %d ignored because lack of enough fields: %s\n", i, line) + } else { + _, _, err = ctx.Login.Client().CreateLabel(ctx.Owner, ctx.Repo, gitea.CreateLabelOption{ + Name: name, + Color: color, + Description: description, + }) + if err != nil { + return err + } + } + + i++ + } + + return nil } func splitLabelLine(line string) (string, string, string) { diff --git a/cmd/labels/create_test.go b/cmd/labels/create_test.go index eacdb2f..f192cea 100644 --- a/cmd/labels/create_test.go +++ b/cmd/labels/create_test.go @@ -20,7 +20,7 @@ func TestParseLabelLine(t *testing.T) { ` scanner := bufio.NewScanner(strings.NewReader(labels)) - var i = 1 + i := 1 for scanner.Scan() { line := scanner.Text() color, name, description := splitLabelLine(line) diff --git a/cmd/labels/update.go b/cmd/labels/update.go index 1b55a90..e1d69db 100644 --- a/cmd/labels/update.go +++ b/cmd/labels/update.go @@ -67,7 +67,6 @@ func runLabelUpdate(_ stdctx.Context, cmd *cli.Command) error { Color: pColor, Description: pDescription, }) - if err != nil { return err } diff --git a/cmd/milestones/create.go b/cmd/milestones/create.go index e6d1865..86180b5 100644 --- a/cmd/milestones/create.go +++ b/cmd/milestones/create.go @@ -67,7 +67,7 @@ func runMilestonesCreate(_ stdctx.Context, cmd *cli.Command) error { 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) { return err } diff --git a/cmd/milestones/issues.go b/cmd/milestones/issues.go index 7d056ba..1f0a3c5 100644 --- a/cmd/milestones/issues.go +++ b/cmd/milestones/issues.go @@ -75,29 +75,23 @@ func runMilestoneIssueList(_ stdctx.Context, cmd *cli.Command) error { ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) client := ctx.Login.Client() - state := gitea.StateOpen - switch ctx.String("state") { - case "all": - state = gitea.StateAll - case "closed": - state = gitea.StateClosed + state, err := flags.ParseState(ctx.String("state")) + if err != nil { + return err } - kind := gitea.IssueTypeAll - switch ctx.String("kind") { - case "issue": - kind = gitea.IssueTypeIssue - case "pull": - kind = gitea.IssueTypePull + kind, err := flags.ParseIssueKind(ctx.String("kind"), gitea.IssueTypeAll) + if err != nil { + return err } if ctx.Args().Len() != 1 { - return fmt.Errorf("Must specify milestone name") + return fmt.Errorf("milestone name is required") } milestone := ctx.Args().First() // make sure milestone exist - _, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, milestone) + _, _, err = client.GetMilestoneByName(ctx.Owner, ctx.Repo, milestone) if err != nil { return err } diff --git a/cmd/milestones/list.go b/cmd/milestones/list.go index e229c56..f09d587 100644 --- a/cmd/milestones/list.go +++ b/cmd/milestones/list.go @@ -48,15 +48,12 @@ func RunMilestonesList(_ stdctx.Context, cmd *cli.Command) error { return err } - state := gitea.StateOpen - switch ctx.String("state") { - case "all": - state = gitea.StateAll - if !cmd.IsSet("fields") { // add to default fields - fields = append(fields, "state") - } - case "closed": - state = gitea.StateClosed + state, err := flags.ParseState(ctx.String("state")) + if err != nil { + return err + } + if state == gitea.StateAll && !cmd.IsSet("fields") { + fields = append(fields, "state") } client := ctx.Login.Client() @@ -64,7 +61,6 @@ func RunMilestonesList(_ stdctx.Context, cmd *cli.Command) error { ListOptions: flags.GetListOptions(), State: state, }) - if err != nil { return err } diff --git a/cmd/milestones/reopen.go b/cmd/milestones/reopen.go index 077ff64..c530600 100644 --- a/cmd/milestones/reopen.go +++ b/cmd/milestones/reopen.go @@ -5,7 +5,6 @@ package milestones import ( stdctx "context" - "errors" "fmt" "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.Ensure(context.CtxRequirement{RemoteRepo: true}) if ctx.Args().Len() == 0 { - return errors.New(ctx.Command.ArgsUsage) + return fmt.Errorf("missing required argument: %s", ctx.Command.ArgsUsage) } state := gitea.StateOpen diff --git a/cmd/organizations/create.go b/cmd/organizations/create.go index 523d03c..da96a22 100644 --- a/cmd/organizations/create.go +++ b/cmd/organizations/create.go @@ -56,7 +56,7 @@ func RunOrganizationCreate(_ stdctx.Context, cmd *cli.Command) error { ctx := context.InitCommand(cmd) 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 diff --git a/cmd/organizations/delete.go b/cmd/organizations/delete.go index b5d8f32..2cdbd2d 100644 --- a/cmd/organizations/delete.go +++ b/cmd/organizations/delete.go @@ -33,7 +33,7 @@ func RunOrganizationDelete(_ stdctx.Context, cmd *cli.Command) error { client := ctx.Login.Client() 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()) diff --git a/cmd/pulls.go b/cmd/pulls.go index e4defa9..e051928 100644 --- a/cmd/pulls.go +++ b/cmd/pulls.go @@ -12,7 +12,6 @@ import ( "code.gitea.io/tea/modules/interact" "code.gitea.io/tea/modules/print" "code.gitea.io/tea/modules/utils" - "code.gitea.io/tea/modules/workaround" "code.gitea.io/sdk/gitea" "github.com/urfave/cli/v3" @@ -67,9 +66,6 @@ func runPullDetail(_ stdctx.Context, cmd *cli.Command, index string) error { if err != nil { return err } - if err := workaround.FixPullHeadSha(client, pr); err != nil { - return err - } reviews, _, err := client.ListPullReviews(ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{ ListOptions: gitea.ListOptions{Page: -1}, diff --git a/cmd/pulls/approve.go b/cmd/pulls/approve.go index e691c4b..2f5529d 100644 --- a/cmd/pulls/approve.go +++ b/cmd/pulls/approve.go @@ -4,16 +4,11 @@ package pulls import ( - "fmt" - "strings" - stdctx "context" "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" "github.com/urfave/cli/v3" ) @@ -26,20 +21,7 @@ var CmdPullsApprove = cli.Command{ ArgsUsage: " []", Action: func(_ stdctx.Context, cmd *cli.Command) error { ctx := context.InitCommand(cmd) - ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) - - 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) + return runPullReview(ctx, gitea.ReviewStateApproved, false) }, Flags: flags.AllDefaultFlags, } diff --git a/cmd/pulls/checkout.go b/cmd/pulls/checkout.go index 31e1867..d6b11eb 100644 --- a/cmd/pulls/checkout.go +++ b/cmd/pulls/checkout.go @@ -40,7 +40,7 @@ func runPullsCheckout(_ stdctx.Context, cmd *cli.Command) error { RemoteRepo: true, }) 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()) if err != nil { diff --git a/cmd/pulls/clean.go b/cmd/pulls/clean.go index 3aaec2a..76194ea 100644 --- a/cmd/pulls/clean.go +++ b/cmd/pulls/clean.go @@ -35,7 +35,7 @@ func runPullsClean(_ stdctx.Context, cmd *cli.Command) error { ctx := context.InitCommand(cmd) ctx.Ensure(context.CtxRequirement{LocalRepo: true}) 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()) diff --git a/cmd/pulls/close.go b/cmd/pulls/close.go index 20ca9f0..74f7a58 100644 --- a/cmd/pulls/close.go +++ b/cmd/pulls/close.go @@ -19,7 +19,7 @@ var CmdPullsClose = cli.Command{ Description: `Change state of one or more pull requests to 'closed'`, ArgsUsage: " [...]", Action: func(ctx context.Context, cmd *cli.Command) error { - var s = gitea.StateClosed + s := gitea.StateClosed return editPullState(ctx, cmd, gitea.EditPullRequestOption{State: &s}) }, Flags: flags.AllDefaultFlags, diff --git a/cmd/pulls/create.go b/cmd/pulls/create.go index 6bd4a38..b1dd6a3 100644 --- a/cmd/pulls/create.go +++ b/cmd/pulls/create.go @@ -48,7 +48,7 @@ func runPullsCreate(_ stdctx.Context, cmd *cli.Command) error { }) // no args -> interactive mode - if ctx.NumFlags() == 0 { + if ctx.IsInteractiveMode() { if err := interact.CreatePull(ctx); err != nil && !interact.IsQuitting(err) { return err } diff --git a/cmd/pulls/edit.go b/cmd/pulls/edit.go index 615dbef..2635344 100644 --- a/cmd/pulls/edit.go +++ b/cmd/pulls/edit.go @@ -20,7 +20,7 @@ func editPullState(_ stdctx.Context, cmd *cli.Command, opts gitea.EditPullReques ctx := context.InitCommand(cmd) ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) 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()) diff --git a/cmd/pulls/list.go b/cmd/pulls/list.go index f63c4fc..a13d7e4 100644 --- a/cmd/pulls/list.go +++ b/cmd/pulls/list.go @@ -33,21 +33,15 @@ func RunPullsList(_ stdctx.Context, cmd *cli.Command) error { ctx := context.InitCommand(cmd) ctx.Ensure(context.CtxRequirement{RemoteRepo: true}) - state := gitea.StateOpen - switch ctx.String("state") { - case "all": - state = gitea.StateAll - case "open": - state = gitea.StateOpen - case "closed": - state = gitea.StateClosed + state, err := flags.ParseState(ctx.String("state")) + if err != nil { + return err } prs, _, err := ctx.Login.Client().ListRepoPullRequests(ctx.Owner, ctx.Repo, gitea.ListPullRequestsOptions{ ListOptions: flags.GetListOptions(), State: state, }) - if err != nil { return err } diff --git a/cmd/pulls/reject.go b/cmd/pulls/reject.go index 277fcd3..b5a75de 100644 --- a/cmd/pulls/reject.go +++ b/cmd/pulls/reject.go @@ -5,15 +5,10 @@ package pulls import ( 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/tea/cmd/flags" + "code.gitea.io/tea/modules/context" "github.com/urfave/cli/v3" ) @@ -25,20 +20,7 @@ var CmdPullsReject = cli.Command{ ArgsUsage: " ", Action: func(_ stdctx.Context, cmd *cli.Command) error { ctx := context.InitCommand(cmd) - ctx.Ensure(context.CtxRequirement{RemoteRepo: 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) + return runPullReview(ctx, gitea.ReviewStateRequestChanges, true) }, Flags: flags.AllDefaultFlags, } diff --git a/cmd/pulls/reopen.go b/cmd/pulls/reopen.go index 682d0f8..f05ea08 100644 --- a/cmd/pulls/reopen.go +++ b/cmd/pulls/reopen.go @@ -20,7 +20,7 @@ var CmdPullsReopen = cli.Command{ Description: `Change state of one or more pull requests to 'open'`, ArgsUsage: " [...]", Action: func(ctx context.Context, cmd *cli.Command) error { - var s = gitea.StateOpen + s := gitea.StateOpen return editPullState(ctx, cmd, gitea.EditPullRequestOption{State: &s}) }, Flags: flags.AllDefaultFlags, diff --git a/cmd/pulls/review_helpers.go b/cmd/pulls/review_helpers.go new file mode 100644 index 0000000..cd539f4 --- /dev/null +++ b/cmd/pulls/review_helpers.go @@ -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) +} diff --git a/cmd/repos/list.go b/cmd/repos/list.go index bea0243..e09d4e5 100644 --- a/cmd/repos/list.go +++ b/cmd/repos/list.go @@ -68,16 +68,23 @@ func RunReposList(_ stdctx.Context, cmd *cli.Command) error { ListOptions: flags.GetListOptions(), StarredByUserID: user.ID, }) + if err != nil { + return err + } } else if teaCmd.Bool("watched") { + var err error rps, _, err = client.GetMyWatchedRepos() // TODO: this does not expose pagination.. + if err != nil { + return err + } } else { + var err error rps, _, err = client.ListMyRepos(gitea.ListReposOptions{ ListOptions: flags.GetListOptions(), }) - } - - if err != nil { - return err + if err != nil { + return err + } } reposFiltered := rps diff --git a/cmd/repos/migrate.go b/cmd/repos/migrate.go index af73ecb..6f13fec 100644 --- a/cmd/repos/migrate.go +++ b/cmd/repos/migrate.go @@ -157,7 +157,6 @@ func runRepoMigrate(_ stdctx.Context, cmd *cli.Command) error { } repo, _, err = client.MigrateRepo(opts) - if err != nil { return err } diff --git a/cmd/repos/search.go b/cmd/repos/search.go index 9b8ad3b..d93a592 100644 --- a/cmd/repos/search.go +++ b/cmd/repos/search.go @@ -62,7 +62,7 @@ func runReposSearch(_ stdctx.Context, cmd *cli.Command) error { var ownerID int64 if teaCmd.IsSet("owner") { - // test if owner is a organisation + // test if owner is an organization org, _, err := client.GetOrg(teaCmd.String("owner")) if err != nil { // HACK: the client does not return a response on 404, so we can't check res.StatusCode diff --git a/cmd/times/list.go b/cmd/times/list.go index 0e8d0e9..9a21c71 100644 --- a/cmd/times/list.go +++ b/cmd/times/list.go @@ -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)", }, timeFieldsFlag, + &flags.PaginationPageFlag, + &flags.PaginationLimitFlag, }, 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() if ctx.Bool("mine") { - times, _, err = client.GetMyTrackedTimes() + times, _, err = client.ListMyTrackedTimes(opts) fields = []string{"created", "repo", "issue", "duration"} } else if user == "" { // 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"} } else if strings.HasPrefix(user, "#") { // get all tracked times on the specified issue - issue, err := utils.ArgToIndex(user) - if err != nil { - return err + issue, parseErr := utils.ArgToIndex(user) + if parseErr != nil { + return parseErr } times, _, err = client.ListIssueTrackedTimes(ctx.Owner, ctx.Repo, issue, opts) fields = []string{"created", "user", "duration"} diff --git a/cmd/webhooks/delete.go b/cmd/webhooks/delete.go index 18226d7..7f7348d 100644 --- a/cmd/webhooks/delete.go +++ b/cmd/webhooks/delete.go @@ -63,7 +63,7 @@ func runWebhooksDelete(ctx stdctx.Context, cmd *cli.Command) error { var response string fmt.Scanln(&response) if response != "y" && response != "Y" && response != "yes" { - fmt.Println("Deletion cancelled.") + fmt.Println("Deletion canceled.") return nil } } diff --git a/cmd/webhooks/delete_test.go b/cmd/webhooks/delete_test.go index a74dcd8..6d1a1a8 100644 --- a/cmd/webhooks/delete_test.go +++ b/cmd/webhooks/delete_test.go @@ -435,9 +435,9 @@ func TestDeleteSuccessMessage(t *testing.T) { } func TestDeleteCancellationMessage(t *testing.T) { - expectedMessage := "Deletion cancelled." + expectedMessage := "Deletion canceled." 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") } diff --git a/docs/CLI.md b/docs/CLI.md index fae47c6..44bf1d6 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -849,12 +849,16 @@ Operate on tracked times of a repository's issues & pulls **--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 **--mine, -m**: Show all times tracked by you across all repositories (overrides command arguments) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) +**--page, -p**="": specify page (default: 1) + **--remote, -R**="": Discover Gitea login from remote. 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 +**--limit, --lm**="": specify limit of items per page (default: 30) + **--login, -l**="": Use a different Gitea Login. Optional **--mine, -m**: Show all times tracked by you across all repositories (overrides command arguments) **--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json) +**--page, -p**="": specify page (default: 1) + **--remote, -R**="": Discover Gitea login from remote. Optional **--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional diff --git a/modules/config/config.go b/modules/config/config.go index 30b59f3..6402697 100644 --- a/modules/config/config.go +++ b/modules/config/config.go @@ -83,14 +83,15 @@ func loadConfig() (err error) { ymlPath := GetConfigPath() exist, _ := utils.FileExist(ymlPath) if exist { - bs, err := os.ReadFile(ymlPath) - if err != nil { - err = fmt.Errorf("Failed to read config file: %s", ymlPath) + bs, readErr := os.ReadFile(ymlPath) + if readErr != nil { + err = fmt.Errorf("failed to read config file %s: %w", ymlPath, readErr) + return } - err = yaml.Unmarshal(bs, &config) - if err != nil { - err = fmt.Errorf("Failed to parse contents of config file: %s", ymlPath) + if unmarshalErr := yaml.Unmarshal(bs, &config); unmarshalErr != nil { + err = fmt.Errorf("failed to parse config file %s: %w", ymlPath, unmarshalErr) + return } } }) diff --git a/modules/context/context.go b/modules/context/context.go index 1a2a61d..8570041 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -46,6 +46,12 @@ func (ctx *TeaContext) GetRemoteRepoHTMLURL() string { 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. func (ctx *TeaContext) Ensure(req CtxRequirement) { if req.LocalRepo && ctx.LocalRepo == nil { diff --git a/modules/git/branch.go b/modules/git/branch.go index d112d29..2f2bc79 100644 --- a/modules/git/branch.go +++ b/modules/git/branch.go @@ -143,7 +143,7 @@ func (r TeaRepo) TeaFindBranchByName(branchName, repoURL string) (b *git_config. defer iter.Close() var remoteRefName 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 { if ref.Name().IsRemote() && ref.Name().Short() == remoteSearchingName { remoteRefName = ref.Name() diff --git a/modules/git/repo_test.go b/modules/git/repo_test.go index 586caaa..b9b339a 100644 --- a/modules/git/repo_test.go +++ b/modules/git/repo_test.go @@ -37,7 +37,7 @@ func TestRepoFromPath_Worktree(t *testing.T) { // Create an initial commit (required for worktree) 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) cmd = exec.Command("git", "-C", mainRepoPath, "add", "README.md") assert.NoError(t, cmd.Run()) diff --git a/modules/interact/pull_merge.go b/modules/interact/pull_merge.go index a47264e..583438e 100644 --- a/modules/interact/pull_merge.go +++ b/modules/interact/pull_merge.go @@ -19,7 +19,7 @@ import ( // MergePull interactively creates a PR func MergePull(ctx *context.TeaContext) error { 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() @@ -51,9 +51,12 @@ func getPullIndex(ctx *context.TeaContext, branch string) (int64, error) { // paginated fetch var prs []*gitea.PullRequest - var err error for { + var err error prs, _, err = c.ListRepoPullRequests(ctx.Owner, ctx.Repo, opts) + if err != nil { + return 0, err + } if len(prs) == 0 { return 0, fmt.Errorf("No open PRs found") } diff --git a/modules/print/notification.go b/modules/print/notification.go index 487a782..4fd457c 100644 --- a/modules/print/notification.go +++ b/modules/print/notification.go @@ -12,7 +12,7 @@ import ( // NotificationsList prints a listing of notification threads 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 { printables[i] = &printableNotification{x} } diff --git a/modules/print/pull.go b/modules/print/pull.go index 1c05b97..179dc25 100644 --- a/modules/print/pull.go +++ b/modules/print/pull.go @@ -111,7 +111,6 @@ func formatReviews(pr *gitea.PullRequest, reviews []*gitea.PullReview) string { 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) { labelMap := map[int64]string{} - var printables = make([]printable, len(pulls)) + printables := make([]printable, len(pulls)) machineReadable := isMachineReadable(output) for i, x := range pulls { @@ -227,13 +226,13 @@ func (x printablePull) FormatField(field string, machineReadable bool) string { } return "" case "labels": - var labels = make([]string, len(x.Labels)) + labels := make([]string, len(x.Labels)) for i, l := range x.Labels { labels[i] = (*x.formattedLabels)[l.ID] } return strings.Join(labels, " ") case "assignees": - var assignees = make([]string, len(x.Assignees)) + assignees := make([]string, len(x.Assignees)) for i, a := range x.Assignees { assignees[i] = formatUserName(a) } diff --git a/modules/print/times.go b/modules/print/times.go index d5022cc..a9d8932 100644 --- a/modules/print/times.go +++ b/modules/print/times.go @@ -11,7 +11,7 @@ import ( // TrackedTimesList print list of tracked times to stdout 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 for i, t := range times { totalDuration += t.Time diff --git a/modules/print/user.go b/modules/print/user.go index 924428b..437538d 100644 --- a/modules/print/user.go +++ b/modules/print/user.go @@ -53,7 +53,7 @@ func UserDetails(user *gitea.User) { // UserList prints a listing of the users 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 { printables[i] = &printableUser{u} } diff --git a/modules/task/login_create.go b/modules/task/login_create.go index 44f3956..523caa1 100644 --- a/modules/task/login_create.go +++ b/modules/task/login_create.go @@ -51,7 +51,7 @@ func CreateLogin(name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCe // checks ... // ... if we have a url 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 diff --git a/modules/task/milestone_create.go b/modules/task/milestone_create.go index d08e778..bbde1df 100644 --- a/modules/task/milestone_create.go +++ b/modules/task/milestone_create.go @@ -15,7 +15,6 @@ import ( // 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 { - // title is required if len(title) == 0 { return fmt.Errorf("Title is required") diff --git a/modules/task/pull_checkout.go b/modules/task/pull_checkout.go index aadbafe..2e6e8b1 100644 --- a/modules/task/pull_checkout.go +++ b/modules/task/pull_checkout.go @@ -9,7 +9,6 @@ import ( "code.gitea.io/sdk/gitea" "code.gitea.io/tea/modules/config" local_git "code.gitea.io/tea/modules/git" - "code.gitea.io/tea/modules/workaround" "github.com/go-git/go-git/v5" git_config "github.com/go-git/go-git/v5/config" @@ -29,9 +28,6 @@ func PullCheckout( if err != nil { return fmt.Errorf("couldn't fetch PR: %s", err) } - if err := workaround.FixPullHeadSha(client, pr); err != nil { - return err - } // FIXME: should use ctx.LocalRepo..? localRepo, err := local_git.RepoForWorkdir() diff --git a/modules/task/pull_clean.go b/modules/task/pull_clean.go index 9c5f78f..c09647c 100644 --- a/modules/task/pull_clean.go +++ b/modules/task/pull_clean.go @@ -8,7 +8,6 @@ import ( "code.gitea.io/tea/modules/config" local_git "code.gitea.io/tea/modules/git" - "code.gitea.io/tea/modules/workaround" "code.gitea.io/sdk/gitea" 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 { return err } - if err := workaround.FixPullHeadSha(client, pr); err != nil { - return err - } if pr.State == gitea.StateOpen { 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 { fmt.Printf("Deleting remote branch %s\n", remoteBranch) - url, err := r.TeaRemoteURL(branch.Remote) - if err != nil { - return err + url, urlErr := r.TeaRemoteURL(branch.Remote) + if urlErr != nil { + return urlErr } - auth, err := local_git.GetAuthForURL(url, login.Token, login.SSHKey, callback) - if err != nil { - return err + auth, authErr := local_git.GetAuthForURL(url, login.Token, login.SSHKey, callback) + if authErr != nil { + return authErr } - err = r.TeaDeleteRemoteBranch(branch.Remote, remoteBranch, auth) + return r.TeaDeleteRemoteBranch(branch.Remote, remoteBranch, auth) } - return err + return nil } diff --git a/modules/task/repo_clone.go b/modules/task/repo_clone.go index 8461e81..1a56147 100644 --- a/modules/task/repo_clone.go +++ b/modules/task/repo_clone.go @@ -40,7 +40,7 @@ func RepoClone( return nil, err } - // default path behaviour as native git + // default path behavior as native git if path == "" { path = repoName } diff --git a/modules/utils/input.go b/modules/utils/input.go new file mode 100644 index 0000000..cd51094 --- /dev/null +++ b/modules/utils/input.go @@ -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 +} diff --git a/modules/utils/path.go b/modules/utils/path.go index 0d713b3..16621aa 100644 --- a/modules/utils/path.go +++ b/modules/utils/path.go @@ -62,9 +62,9 @@ func AbsPathWithExpansion(p string) (string, error) { } if p == "~" { return u.HomeDir, nil - } else if strings.HasPrefix(p, "~/") { - return filepath.Join(u.HomeDir, p[2:]), nil - } else { - return filepath.Abs(p) } + if strings.HasPrefix(p, "~/") { + return filepath.Join(u.HomeDir, p[2:]), nil + } + return filepath.Abs(p) } diff --git a/modules/workaround/pull.go b/modules/workaround/pull.go deleted file mode 100644 index 81f9b75..0000000 --- a/modules/workaround/pull.go +++ /dev/null @@ -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 -}