mirror of
https://gitea.com/gitea/tea.git
synced 2026-04-25 17:53:37 +02:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5103496232 | ||
|
|
a58c35c3e2 | ||
|
|
783ac7684a | ||
|
|
d0b7ea09e8 | ||
|
|
20914a1375 | ||
|
|
3c1c9b2904 | ||
|
|
63bc90ea52 | ||
|
|
9e0a6203ae | ||
|
|
84ecd16f9c | ||
|
|
53e53e1067 | ||
|
|
0489d8c275 | ||
|
|
f538c05282 | ||
|
|
662e339bf9 |
@@ -39,7 +39,7 @@ jobs:
|
||||
make unit-test-coverage
|
||||
services:
|
||||
gitea:
|
||||
image: docker.gitea.com/gitea:1.25.5
|
||||
image: docker.gitea.com/gitea:1.26.0
|
||||
cmd:
|
||||
- bash
|
||||
- -c
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -17,3 +17,5 @@ dist/
|
||||
.direnv/
|
||||
result
|
||||
result-*
|
||||
|
||||
.DS_Store
|
||||
@@ -20,6 +20,10 @@ var CmdActionsWorkflows = cli.Command{
|
||||
Action: runWorkflowsDefault,
|
||||
Commands: []*cli.Command{
|
||||
&workflows.CmdWorkflowsList,
|
||||
&workflows.CmdWorkflowsView,
|
||||
&workflows.CmdWorkflowsDispatch,
|
||||
&workflows.CmdWorkflowsEnable,
|
||||
&workflows.CmdWorkflowsDisable,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
65
cmd/actions/workflows/disable.go
Normal file
65
cmd/actions/workflows/disable.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package workflows
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdWorkflowsDisable represents a sub command to disable a workflow
|
||||
var CmdWorkflowsDisable = cli.Command{
|
||||
Name: "disable",
|
||||
Usage: "Disable a workflow",
|
||||
Description: "Disable a workflow in the repository",
|
||||
ArgsUsage: "<workflow-id>",
|
||||
Action: runWorkflowsDisable,
|
||||
Flags: append([]cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "confirm",
|
||||
Aliases: []string{"y"},
|
||||
Usage: "confirm disable without prompting",
|
||||
},
|
||||
}, flags.AllDefaultFlags...),
|
||||
}
|
||||
|
||||
func runWorkflowsDisable(ctx stdctx.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Len() == 0 {
|
||||
return fmt.Errorf("workflow ID is required")
|
||||
}
|
||||
|
||||
c, err := context.InitCommand(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
client := c.Login.Client()
|
||||
|
||||
workflowID := cmd.Args().First()
|
||||
|
||||
if !cmd.Bool("confirm") {
|
||||
fmt.Printf("Are you sure you want to disable workflow %s? [y/N] ", workflowID)
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
if response != "y" && response != "Y" && response != "yes" {
|
||||
fmt.Println("Disable canceled.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
_, err = client.DisableRepoActionWorkflow(c.Owner, c.Repo, workflowID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to disable workflow: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Workflow %s disabled successfully\n", workflowID)
|
||||
return nil
|
||||
}
|
||||
174
cmd/actions/workflows/dispatch.go
Normal file
174
cmd/actions/workflows/dispatch.go
Normal file
@@ -0,0 +1,174 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package workflows
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
"code.gitea.io/tea/modules/print"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdWorkflowsDispatch represents a sub command to dispatch a workflow
|
||||
var CmdWorkflowsDispatch = cli.Command{
|
||||
Name: "dispatch",
|
||||
Aliases: []string{"trigger", "run"},
|
||||
Usage: "Dispatch a workflow run",
|
||||
Description: "Trigger a workflow_dispatch event for a workflow",
|
||||
ArgsUsage: "<workflow-id>",
|
||||
Action: runWorkflowsDispatch,
|
||||
Flags: append([]cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "ref",
|
||||
Aliases: []string{"r"},
|
||||
Usage: "branch or tag to dispatch on (default: current branch)",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "input",
|
||||
Aliases: []string{"i"},
|
||||
Usage: "workflow input in key=value format (can be specified multiple times)",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "follow",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "follow log output after dispatching",
|
||||
},
|
||||
}, flags.AllDefaultFlags...),
|
||||
}
|
||||
|
||||
func runWorkflowsDispatch(ctx stdctx.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Len() == 0 {
|
||||
return fmt.Errorf("workflow ID is required")
|
||||
}
|
||||
|
||||
c, err := context.InitCommand(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
client := c.Login.Client()
|
||||
|
||||
workflowID := cmd.Args().First()
|
||||
|
||||
ref := cmd.String("ref")
|
||||
if ref == "" {
|
||||
if c.LocalRepo != nil {
|
||||
branchName, _, localErr := c.LocalRepo.TeaGetCurrentBranchNameAndSHA()
|
||||
if localErr == nil && branchName != "" {
|
||||
ref = branchName
|
||||
}
|
||||
}
|
||||
if ref == "" {
|
||||
return fmt.Errorf("--ref is required (no local branch detected)")
|
||||
}
|
||||
}
|
||||
|
||||
inputs := make(map[string]string)
|
||||
for _, input := range cmd.StringSlice("input") {
|
||||
key, value, ok := strings.Cut(input, "=")
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid input format %q, expected key=value", input)
|
||||
}
|
||||
inputs[key] = value
|
||||
}
|
||||
|
||||
opt := gitea.CreateActionWorkflowDispatchOption{
|
||||
Ref: ref,
|
||||
Inputs: inputs,
|
||||
}
|
||||
|
||||
details, _, err := client.DispatchRepoActionWorkflow(c.Owner, c.Repo, workflowID, opt, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to dispatch workflow: %w", err)
|
||||
}
|
||||
|
||||
print.ActionWorkflowDispatchResult(details)
|
||||
|
||||
if cmd.Bool("follow") && details != nil && details.WorkflowRunID > 0 {
|
||||
return followDispatchedRun(client, c, details.WorkflowRunID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
followPollInterval = 2 * time.Second
|
||||
followMaxDuration = 30 * time.Minute
|
||||
)
|
||||
|
||||
// followDispatchedRun waits for the dispatched run to start, then follows its logs
|
||||
func followDispatchedRun(client *gitea.Client, c *context.TeaContext, runID int64) error {
|
||||
fmt.Printf("\nWaiting for run %d to start...\n", runID)
|
||||
|
||||
var jobs *gitea.ActionWorkflowJobsResponse
|
||||
for range 30 {
|
||||
time.Sleep(followPollInterval)
|
||||
|
||||
var err error
|
||||
jobs, _, err = client.ListRepoActionRunJobs(c.Owner, c.Repo, runID, gitea.ListRepoActionJobsOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get jobs: %w", err)
|
||||
}
|
||||
if len(jobs.Jobs) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if jobs == nil || len(jobs.Jobs) == 0 {
|
||||
return fmt.Errorf("timed out waiting for jobs to appear")
|
||||
}
|
||||
|
||||
jobID := jobs.Jobs[0].ID
|
||||
jobName := jobs.Jobs[0].Name
|
||||
fmt.Printf("Following logs for job '%s' (ID: %d) - press Ctrl+C to stop...\n", jobName, jobID)
|
||||
fmt.Println("---")
|
||||
|
||||
deadline := time.Now().Add(followMaxDuration)
|
||||
var lastLogLength int
|
||||
for time.Now().Before(deadline) {
|
||||
job, _, err := client.GetRepoActionJob(c.Owner, c.Repo, jobID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get job: %w", err)
|
||||
}
|
||||
|
||||
isRunning := job.Status == "in_progress" || job.Status == "queued" || job.Status == "pending"
|
||||
|
||||
logs, _, logErr := client.GetRepoActionJobLogs(c.Owner, c.Repo, jobID)
|
||||
if logErr != nil && isRunning {
|
||||
time.Sleep(followPollInterval)
|
||||
continue
|
||||
}
|
||||
|
||||
if logErr == nil && len(logs) > lastLogLength {
|
||||
fmt.Print(string(logs[lastLogLength:]))
|
||||
lastLogLength = len(logs)
|
||||
}
|
||||
|
||||
if !isRunning {
|
||||
if logErr != nil {
|
||||
fmt.Printf("\n---\nJob completed with status: %s (failed to fetch final logs: %v)\n", job.Status, logErr)
|
||||
} else {
|
||||
fmt.Printf("\n---\nJob completed with status: %s\n", job.Status)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(followPollInterval)
|
||||
}
|
||||
|
||||
if time.Now().After(deadline) {
|
||||
return fmt.Errorf("timed out after %s following logs", followMaxDuration)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
48
cmd/actions/workflows/enable.go
Normal file
48
cmd/actions/workflows/enable.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package workflows
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdWorkflowsEnable represents a sub command to enable a workflow
|
||||
var CmdWorkflowsEnable = cli.Command{
|
||||
Name: "enable",
|
||||
Usage: "Enable a workflow",
|
||||
Description: "Enable a disabled workflow in the repository",
|
||||
ArgsUsage: "<workflow-id>",
|
||||
Action: runWorkflowsEnable,
|
||||
Flags: flags.AllDefaultFlags,
|
||||
}
|
||||
|
||||
func runWorkflowsEnable(ctx stdctx.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Len() == 0 {
|
||||
return fmt.Errorf("workflow ID is required")
|
||||
}
|
||||
|
||||
c, err := context.InitCommand(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
client := c.Login.Client()
|
||||
|
||||
workflowID := cmd.Args().First()
|
||||
_, err = client.EnableRepoActionWorkflow(c.Owner, c.Repo, workflowID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to enable workflow: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Workflow %s enabled successfully\n", workflowID)
|
||||
return nil
|
||||
}
|
||||
@@ -6,8 +6,6 @@ package workflows
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
@@ -22,15 +20,12 @@ var CmdWorkflowsList = cli.Command{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "List repository workflows",
|
||||
Description: "List workflow files in the repository with active/inactive status",
|
||||
Description: "List workflows in the repository with their status",
|
||||
Action: RunWorkflowsList,
|
||||
Flags: append([]cli.Flag{
|
||||
&flags.PaginationPageFlag,
|
||||
&flags.PaginationLimitFlag,
|
||||
}, flags.AllDefaultFlags...),
|
||||
Flags: flags.AllDefaultFlags,
|
||||
}
|
||||
|
||||
// RunWorkflowsList lists workflow files in the repository
|
||||
// RunWorkflowsList lists workflows in the repository using the workflow API
|
||||
func RunWorkflowsList(ctx stdctx.Context, cmd *cli.Command) error {
|
||||
c, err := context.InitCommand(cmd)
|
||||
if err != nil {
|
||||
@@ -41,51 +36,15 @@ func RunWorkflowsList(ctx stdctx.Context, cmd *cli.Command) error {
|
||||
}
|
||||
client := c.Login.Client()
|
||||
|
||||
// Try to list workflow files from .gitea/workflows directory
|
||||
var workflows []*gitea.ContentsResponse
|
||||
|
||||
// Try .gitea/workflows first, then .github/workflows
|
||||
workflowDir := ".gitea/workflows"
|
||||
contents, _, err := client.ListContents(c.Owner, c.Repo, "", workflowDir)
|
||||
resp, _, err := client.ListRepoActionWorkflows(c.Owner, c.Repo)
|
||||
if err != nil {
|
||||
workflowDir = ".github/workflows"
|
||||
contents, _, err = client.ListContents(c.Owner, c.Repo, "", workflowDir)
|
||||
if err != nil {
|
||||
fmt.Printf("No workflow files found\n")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to list workflows: %w", err)
|
||||
}
|
||||
|
||||
// Filter for workflow files (.yml and .yaml)
|
||||
for _, content := range contents {
|
||||
if content.Type == "file" {
|
||||
ext := strings.ToLower(filepath.Ext(content.Name))
|
||||
if ext == ".yml" || ext == ".yaml" {
|
||||
content.Path = workflowDir + "/" + content.Name
|
||||
workflows = append(workflows, content)
|
||||
}
|
||||
}
|
||||
var workflows []*gitea.ActionWorkflow
|
||||
if resp != nil {
|
||||
workflows = resp.Workflows
|
||||
}
|
||||
|
||||
if len(workflows) == 0 {
|
||||
fmt.Printf("No workflow files found\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check which workflows have runs to determine active status
|
||||
workflowStatus := make(map[string]bool)
|
||||
|
||||
// Get recent runs to check activity
|
||||
runs, _, err := client.ListRepoActionRuns(c.Owner, c.Repo, gitea.ListRepoActionRunsOptions{
|
||||
ListOptions: flags.GetListOptions(cmd),
|
||||
})
|
||||
if err == nil && runs != nil {
|
||||
for _, run := range runs.WorkflowRuns {
|
||||
// Extract workflow file name from path
|
||||
workflowFile := filepath.Base(run.Path)
|
||||
workflowStatus[workflowFile] = true
|
||||
}
|
||||
}
|
||||
|
||||
return print.WorkflowsList(workflows, workflowStatus, c.Output)
|
||||
return print.ActionWorkflowsList(workflows, c.Output)
|
||||
}
|
||||
|
||||
50
cmd/actions/workflows/view.go
Normal file
50
cmd/actions/workflows/view.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package workflows
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
"code.gitea.io/tea/modules/print"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdWorkflowsView represents a sub command to view workflow details
|
||||
var CmdWorkflowsView = cli.Command{
|
||||
Name: "view",
|
||||
Aliases: []string{"show", "get"},
|
||||
Usage: "View workflow details",
|
||||
Description: "View details of a specific workflow",
|
||||
ArgsUsage: "<workflow-id>",
|
||||
Action: runWorkflowsView,
|
||||
Flags: flags.AllDefaultFlags,
|
||||
}
|
||||
|
||||
func runWorkflowsView(ctx stdctx.Context, cmd *cli.Command) error {
|
||||
if cmd.Args().Len() == 0 {
|
||||
return fmt.Errorf("workflow ID is required")
|
||||
}
|
||||
|
||||
c, err := context.InitCommand(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
client := c.Login.Client()
|
||||
|
||||
workflowID := cmd.Args().First()
|
||||
wf, _, err := client.GetRepoActionWorkflow(c.Owner, c.Repo, workflowID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get workflow: %w", err)
|
||||
}
|
||||
|
||||
print.ActionWorkflowDetails(wf)
|
||||
return nil
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
"code.gitea.io/tea/cmd/releases"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
@@ -37,15 +38,15 @@ func runReleaseAttachmentCreate(_ stdctx.Context, cmd *cli.Command) error {
|
||||
client := ctx.Login.Client()
|
||||
|
||||
if ctx.Args().Len() < 2 {
|
||||
return fmt.Errorf("No release tag or assets specified.\nUsage:\t%s", ctx.Command.UsageText)
|
||||
return fmt.Errorf("no release tag or assets specified.\nUsage:\t%s", ctx.Command.UsageText)
|
||||
}
|
||||
|
||||
tag := ctx.Args().First()
|
||||
if len(tag) == 0 {
|
||||
return fmt.Errorf("Release tag needed to create attachment")
|
||||
return fmt.Errorf("release tag needed to create attachment")
|
||||
}
|
||||
|
||||
release, err := getReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
||||
release, err := releases.GetReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
"code.gitea.io/tea/cmd/releases"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
@@ -42,12 +43,12 @@ func runReleaseAttachmentDelete(_ stdctx.Context, cmd *cli.Command) error {
|
||||
client := ctx.Login.Client()
|
||||
|
||||
if ctx.Args().Len() < 2 {
|
||||
return fmt.Errorf("No release tag or attachment names specified.\nUsage:\t%s", ctx.Command.UsageText)
|
||||
return fmt.Errorf("no release tag or attachment names specified.\nUsage:\t%s", ctx.Command.UsageText)
|
||||
}
|
||||
|
||||
tag := ctx.Args().First()
|
||||
if len(tag) == 0 {
|
||||
return fmt.Errorf("Release tag needed to delete attachment")
|
||||
return fmt.Errorf("release tag needed to delete attachment")
|
||||
}
|
||||
|
||||
if !ctx.Bool("confirm") {
|
||||
@@ -55,17 +56,25 @@ func runReleaseAttachmentDelete(_ stdctx.Context, cmd *cli.Command) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
release, err := getReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
||||
release, err := releases.GetReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existing, _, err := client.ListReleaseAttachments(ctx.Owner, ctx.Repo, release.ID, gitea.ListReleaseAttachmentsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: -1},
|
||||
var existing []*gitea.Attachment
|
||||
for page := 1; ; {
|
||||
page_attachments, resp, err := client.ListReleaseAttachments(ctx.Owner, ctx.Repo, release.ID, gitea.ListReleaseAttachmentsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
existing = append(existing, page_attachments...)
|
||||
if resp == nil || resp.NextPage == 0 {
|
||||
break
|
||||
}
|
||||
page = resp.NextPage
|
||||
}
|
||||
|
||||
for _, name := range ctx.Args().Slice()[1:] {
|
||||
var attachment *gitea.Attachment
|
||||
@@ -75,7 +84,7 @@ func runReleaseAttachmentDelete(_ stdctx.Context, cmd *cli.Command) error {
|
||||
}
|
||||
}
|
||||
if attachment == nil {
|
||||
return fmt.Errorf("Release does not have attachment named '%s'", name)
|
||||
return fmt.Errorf("release does not have attachment named '%s'", name)
|
||||
}
|
||||
|
||||
_, err = client.DeleteReleaseAttachment(ctx.Owner, ctx.Repo, release.ID, attachment.ID)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
"code.gitea.io/tea/cmd/releases"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
"code.gitea.io/tea/modules/print"
|
||||
|
||||
@@ -42,10 +43,10 @@ func RunReleaseAttachmentList(_ stdctx.Context, cmd *cli.Command) error {
|
||||
|
||||
tag := ctx.Args().First()
|
||||
if len(tag) == 0 {
|
||||
return fmt.Errorf("Release tag needed to list attachments")
|
||||
return fmt.Errorf("release tag needed to list attachments")
|
||||
}
|
||||
|
||||
release, err := getReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
||||
release, err := releases.GetReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -59,21 +60,3 @@ func RunReleaseAttachmentList(_ stdctx.Context, cmd *cli.Command) error {
|
||||
|
||||
return print.ReleaseAttachmentsList(attachments, ctx.Output)
|
||||
}
|
||||
|
||||
func getReleaseByTag(owner, repo, tag string, client *gitea.Client) (*gitea.Release, error) {
|
||||
rl, _, err := client.ListReleases(owner, repo, gitea.ListReleasesOptions{
|
||||
ListOptions: gitea.ListOptions{Page: -1},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rl) == 0 {
|
||||
return nil, fmt.Errorf("Repo does not have any release")
|
||||
}
|
||||
for _, r := range rl {
|
||||
if r.TagName == tag {
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("Release tag does not exist")
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ var CmdBranches = cli.Command{
|
||||
&branches.CmdBranchesList,
|
||||
&branches.CmdBranchesProtect,
|
||||
&branches.CmdBranchesUnprotect,
|
||||
&branches.CmdBranchesRename,
|
||||
},
|
||||
Flags: append([]cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
|
||||
78
cmd/branches/rename.go
Normal file
78
cmd/branches/rename.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package branches
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdBranchesRenameFlags Flags for command rename
|
||||
var CmdBranchesRenameFlags = append([]cli.Flag{
|
||||
branchFieldsFlag,
|
||||
&flags.PaginationPageFlag,
|
||||
&flags.PaginationLimitFlag,
|
||||
}, flags.AllDefaultFlags...)
|
||||
|
||||
// CmdBranchesRename represents a sub command of branches to rename a branch
|
||||
var CmdBranchesRename = cli.Command{
|
||||
Name: "rename",
|
||||
Aliases: []string{"rn"},
|
||||
Usage: "Rename a branch",
|
||||
Description: `Rename a branch in a repository`,
|
||||
ArgsUsage: "<old_branch_name> <new_branch_name>",
|
||||
Action: RunBranchesRename,
|
||||
Flags: CmdBranchesRenameFlags,
|
||||
}
|
||||
|
||||
// RunBranchesRename function to rename a branch
|
||||
func RunBranchesRename(_ stdctx.Context, cmd *cli.Command) error {
|
||||
ctx, err := context.InitCommand(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ValidateRenameArgs(ctx.Args().Slice()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldBranchName := ctx.Args().Get(0)
|
||||
newBranchName := ctx.Args().Get(1)
|
||||
|
||||
owner := ctx.Owner
|
||||
if ctx.IsSet("owner") {
|
||||
owner = ctx.String("owner")
|
||||
}
|
||||
|
||||
successful, _, err := ctx.Login.Client().RenameRepoBranch(owner, ctx.Repo, oldBranchName, gitea.RenameRepoBranchOption{
|
||||
Name: newBranchName,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to rename branch: %w", err)
|
||||
}
|
||||
if !successful {
|
||||
return fmt.Errorf("failed to rename branch")
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully renamed branch '%s' to '%s'\n", oldBranchName, newBranchName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateRenameArgs validates arguments for the rename command
|
||||
func ValidateRenameArgs(args []string) error {
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("must specify exactly two arguments: <old_branch_name> <new_branch_name>")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
46
cmd/branches/rename_test.go
Normal file
46
cmd/branches/rename_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package branches
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBranchesRenameArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid args",
|
||||
args: []string{"main", "develop"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "missing both args",
|
||||
args: []string{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "missing new branch name",
|
||||
args: []string{"main"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "too many args",
|
||||
args: []string{"main", "develop", "extra"},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidateRenameArgs(tt.args)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateRenameArgs() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -76,9 +76,13 @@ func runRepoClone(ctx stdctx.Context, cmd *cli.Command) error {
|
||||
|
||||
owner, repo = utils.GetOwnerAndRepo(url.Path, login.User)
|
||||
if url.Host != "" {
|
||||
login = config.GetLoginByHost(url.Host)
|
||||
var lookupErr error
|
||||
login, lookupErr = config.GetLoginByHost(url.Host)
|
||||
if lookupErr != nil {
|
||||
return lookupErr
|
||||
}
|
||||
if login == nil {
|
||||
return fmt.Errorf("No login configured matching host '%s', run `tea login add` first", url.Host)
|
||||
return fmt.Errorf("no login configured matching host '%s', run 'tea login add' first", url.Host)
|
||||
}
|
||||
debug.Printf("Matched login '%s' for host '%s'", login.Name, url.Host)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func runAddComment(_ stdctx.Context, cmd *cli.Command) error {
|
||||
|
||||
args := ctx.Args()
|
||||
if args.Len() == 0 {
|
||||
return fmt.Errorf("Please specify issue / pr index")
|
||||
return fmt.Errorf("please specify issue / pr index")
|
||||
}
|
||||
|
||||
idx, err := utils.ArgToIndex(ctx.Args().First())
|
||||
|
||||
@@ -44,7 +44,7 @@ func (f CsvFlag) GetValues(cmd *cli.Command) ([]string, error) {
|
||||
if f.AvailableFields != nil && val != "" {
|
||||
for _, field := range selection {
|
||||
if !utils.Contains(f.AvailableFields, field) {
|
||||
return nil, fmt.Errorf("Invalid field '%s'", field)
|
||||
return nil, fmt.Errorf("invalid field '%s'", field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package flags
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"github.com/urfave/cli/v3"
|
||||
@@ -167,7 +168,7 @@ func ParseState(stateStr string) (gitea.StateType, error) {
|
||||
case "closed":
|
||||
return gitea.StateClosed, nil
|
||||
default:
|
||||
return "", errors.New("unknown state '" + stateStr + "'")
|
||||
return "", fmt.Errorf("unknown state '%s'", stateStr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +185,6 @@ func ParseIssueKind(kindStr string, defaultKind gitea.IssueType) (gitea.IssueTyp
|
||||
case "pull", "pulls", "pr":
|
||||
return gitea.IssueTypePull, nil
|
||||
default:
|
||||
return "", errors.New("unknown kind '" + kindStr + "'")
|
||||
return "", fmt.Errorf("unknown kind '%s'", kindStr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ func GetIssuePRCreateFlags(ctx *context.TeaContext) (*gitea.CreateIssueOption, e
|
||||
}
|
||||
ms, _, err := client.GetMilestoneByName(ctx.Owner, ctx.Repo, milestoneName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Milestone '%s' not found", milestoneName)
|
||||
return nil, fmt.Errorf("milestone '%s' not found", milestoneName)
|
||||
}
|
||||
opts.Milestone = ms.ID
|
||||
}
|
||||
|
||||
@@ -37,5 +37,5 @@ func runLabels(ctx context.Context, cmd *cli.Command) error {
|
||||
}
|
||||
|
||||
func runLabelsDetails(cmd *cli.Command) error {
|
||||
return fmt.Errorf("Not yet implemented")
|
||||
return fmt.Errorf("not yet implemented")
|
||||
}
|
||||
|
||||
@@ -5,8 +5,7 @@ package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/tea/modules/config"
|
||||
|
||||
@@ -27,7 +26,7 @@ var CmdLoginDelete = cli.Command{
|
||||
func RunLoginDelete(_ context.Context, cmd *cli.Command) error {
|
||||
logins, err := config.GetLogins()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
|
||||
var name string
|
||||
@@ -37,7 +36,7 @@ func RunLoginDelete(_ context.Context, cmd *cli.Command) error {
|
||||
} else if len(logins) == 1 {
|
||||
name = logins[0].Name
|
||||
} else {
|
||||
return errors.New("Please specify a login name")
|
||||
return fmt.Errorf("please specify a login name")
|
||||
}
|
||||
|
||||
return config.DeleteLogin(name)
|
||||
|
||||
@@ -5,7 +5,6 @@ package login
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
@@ -34,7 +33,7 @@ func runLoginEdit(_ context.Context, _ *cli.Command) error {
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
return open.Start(config.GetConfigPath())
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -93,7 +92,7 @@ var CmdLoginHelper = cli.Command{
|
||||
}
|
||||
|
||||
if len(wants["host"]) == 0 {
|
||||
log.Fatal("Hostname is required")
|
||||
return fmt.Errorf("hostname is required")
|
||||
} else if len(wants["protocol"]) == 0 {
|
||||
wants["protocol"] = "http"
|
||||
}
|
||||
@@ -104,20 +103,24 @@ var CmdLoginHelper = cli.Command{
|
||||
var lookupErr error
|
||||
userConfig, lookupErr = config.GetLoginByName(loginName)
|
||||
if lookupErr != nil {
|
||||
log.Fatal(lookupErr)
|
||||
return lookupErr
|
||||
}
|
||||
if userConfig == nil {
|
||||
log.Fatalf("Login '%s' not found", loginName)
|
||||
return fmt.Errorf("login '%s' not found", loginName)
|
||||
}
|
||||
} else {
|
||||
userConfig = config.GetLoginByHost(wants["host"])
|
||||
var lookupErr error
|
||||
userConfig, lookupErr = config.GetLoginByHost(wants["host"])
|
||||
if lookupErr != nil {
|
||||
return lookupErr
|
||||
}
|
||||
if userConfig == nil {
|
||||
log.Fatalf("No login found for host '%s'", wants["host"])
|
||||
return fmt.Errorf("no login found for host '%s'", wants["host"])
|
||||
}
|
||||
}
|
||||
|
||||
if len(userConfig.GetAccessToken()) == 0 {
|
||||
log.Fatal("User not set")
|
||||
return fmt.Errorf("user not set")
|
||||
}
|
||||
|
||||
host, err := url.Parse(userConfig.URL)
|
||||
|
||||
16
cmd/pulls.go
16
cmd/pulls.go
@@ -77,6 +77,9 @@ var CmdPulls = cli.Command{
|
||||
&pulls.CmdPullsApprove,
|
||||
&pulls.CmdPullsReject,
|
||||
&pulls.CmdPullsMerge,
|
||||
&pulls.CmdPullsReviewComments,
|
||||
&pulls.CmdPullsResolve,
|
||||
&pulls.CmdPullsUnresolve,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -106,11 +109,20 @@ func runPullDetail(_ stdctx.Context, cmd *cli.Command, index string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
reviews, _, err := client.ListPullReviews(ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: -1},
|
||||
var reviews []*gitea.PullReview
|
||||
for page := 1; ; {
|
||||
page_reviews, resp, err := client.ListPullReviews(ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("error while loading reviews: %v\n", err)
|
||||
break
|
||||
}
|
||||
reviews = append(reviews, page_reviews...)
|
||||
if resp == nil || resp.NextPage == 0 {
|
||||
break
|
||||
}
|
||||
page = resp.NextPage
|
||||
}
|
||||
|
||||
if ctx.IsSet("output") {
|
||||
|
||||
@@ -5,6 +5,8 @@ package pulls
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
@@ -43,7 +45,8 @@ func RunPullsList(_ stdctx.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
prs, _, err := ctx.Login.Client().ListRepoPullRequests(ctx.Owner, ctx.Repo, gitea.ListPullRequestsOptions{
|
||||
client := ctx.Login.Client()
|
||||
prs, _, err := client.ListRepoPullRequests(ctx.Owner, ctx.Repo, gitea.ListPullRequestsOptions{
|
||||
ListOptions: flags.GetListOptions(cmd),
|
||||
State: state,
|
||||
})
|
||||
@@ -56,5 +59,21 @@ func RunPullsList(_ stdctx.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return print.PullsList(prs, ctx.Output, fields)
|
||||
var ciStatuses map[int64]*gitea.CombinedStatus
|
||||
if slices.Contains(fields, "ci") {
|
||||
ciStatuses = map[int64]*gitea.CombinedStatus{}
|
||||
for _, pr := range prs {
|
||||
if pr.Head == nil || pr.Head.Sha == "" {
|
||||
continue
|
||||
}
|
||||
ci, _, err := client.GetCombinedStatus(ctx.Owner, ctx.Repo, pr.Head.Sha)
|
||||
if err != nil {
|
||||
fmt.Printf("error fetching CI status for PR #%d: %v\n", pr.Index, err)
|
||||
continue
|
||||
}
|
||||
ciStatuses[pr.Index] = ci
|
||||
}
|
||||
}
|
||||
|
||||
return print.PullsList(prs, ctx.Output, fields, ciStatuses)
|
||||
}
|
||||
|
||||
30
cmd/pulls/resolve.go
Normal file
30
cmd/pulls/resolve.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pulls
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
"code.gitea.io/tea/modules/task"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdPullsResolve resolves a review comment on a pull request
|
||||
var CmdPullsResolve = cli.Command{
|
||||
Name: "resolve",
|
||||
Usage: "Resolve a review comment on a pull request",
|
||||
Description: "Resolve a review comment on a pull request",
|
||||
ArgsUsage: "<comment id>",
|
||||
Action: func(_ stdctx.Context, cmd *cli.Command) error {
|
||||
ctx, err := context.InitCommand(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runResolveComment(ctx, task.ResolvePullReviewComment)
|
||||
},
|
||||
Flags: flags.AllDefaultFlags,
|
||||
}
|
||||
63
cmd/pulls/review_comments.go
Normal file
63
cmd/pulls/review_comments.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pulls
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
"code.gitea.io/tea/modules/print"
|
||||
"code.gitea.io/tea/modules/task"
|
||||
"code.gitea.io/tea/modules/utils"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
var reviewCommentFieldsFlag = flags.FieldsFlag(print.PullReviewCommentFields, []string{
|
||||
"id", "path", "line", "body", "reviewer", "resolver",
|
||||
})
|
||||
|
||||
// CmdPullsReviewComments lists review comments on a pull request
|
||||
var CmdPullsReviewComments = cli.Command{
|
||||
Name: "review-comments",
|
||||
Aliases: []string{"rc"},
|
||||
Usage: "List review comments on a pull request",
|
||||
Description: "List review comments on a pull request",
|
||||
ArgsUsage: "<pull index>",
|
||||
Action: runPullsReviewComments,
|
||||
Flags: append([]cli.Flag{reviewCommentFieldsFlag}, flags.AllDefaultFlags...),
|
||||
}
|
||||
|
||||
func runPullsReviewComments(_ stdctx.Context, cmd *cli.Command) error {
|
||||
ctx, err := context.InitCommand(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctx.Args().Len() < 1 {
|
||||
return fmt.Errorf("pull request index is required")
|
||||
}
|
||||
|
||||
idx, err := utils.ArgToIndex(ctx.Args().First())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
comments, err := task.ListPullReviewComments(ctx, idx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fields, err := reviewCommentFieldsFlag.GetValues(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return print.PullReviewCommentsList(comments, ctx.Output, fields)
|
||||
}
|
||||
@@ -40,3 +40,21 @@ func runPullReview(ctx *context.TeaContext, state gitea.ReviewStateType, require
|
||||
|
||||
return task.CreatePullReview(ctx, idx, state, comment, nil)
|
||||
}
|
||||
|
||||
// runResolveComment handles the common logic for resolving/unresolving review comments
|
||||
func runResolveComment(ctx *context.TeaContext, action func(*context.TeaContext, int64) error) error {
|
||||
if err := ctx.Ensure(context.CtxRequirement{RemoteRepo: true}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctx.Args().Len() < 1 {
|
||||
return fmt.Errorf("comment ID is required")
|
||||
}
|
||||
|
||||
commentID, err := utils.ArgToIndex(ctx.Args().First())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return action(ctx, commentID)
|
||||
}
|
||||
|
||||
30
cmd/pulls/unresolve.go
Normal file
30
cmd/pulls/unresolve.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package pulls
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
"code.gitea.io/tea/modules/task"
|
||||
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdPullsUnresolve unresolves a review comment on a pull request
|
||||
var CmdPullsUnresolve = cli.Command{
|
||||
Name: "unresolve",
|
||||
Usage: "Unresolve a review comment on a pull request",
|
||||
Description: "Unresolve a review comment on a pull request",
|
||||
ArgsUsage: "<comment id>",
|
||||
Action: func(_ stdctx.Context, cmd *cli.Command) error {
|
||||
ctx, err := context.InitCommand(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runResolveComment(ctx, task.UnresolvePullReviewComment)
|
||||
},
|
||||
Flags: flags.AllDefaultFlags,
|
||||
}
|
||||
@@ -104,7 +104,7 @@ func runReleaseCreate(_ stdctx.Context, cmd *cli.Command) error {
|
||||
})
|
||||
if err != nil {
|
||||
if resp != nil && resp.StatusCode == http.StatusConflict {
|
||||
return fmt.Errorf("There already is a release for this tag")
|
||||
return fmt.Errorf("there is already a release for this tag")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func runReleaseDelete(_ stdctx.Context, cmd *cli.Command) error {
|
||||
}
|
||||
|
||||
for _, tag := range ctx.Args().Slice() {
|
||||
release, err := getReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
||||
release, err := GetReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ func runReleaseEdit(_ stdctx.Context, cmd *cli.Command) error {
|
||||
}
|
||||
|
||||
for _, tag := range ctx.Args().Slice() {
|
||||
release, err := getReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
||||
release, err := GetReleaseByTag(ctx.Owner, ctx.Repo, tag, client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ package releases
|
||||
|
||||
import (
|
||||
stdctx "context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/tea/cmd/flags"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
@@ -48,21 +47,3 @@ func RunReleasesList(_ stdctx.Context, cmd *cli.Command) error {
|
||||
|
||||
return print.ReleasesList(releases, ctx.Output)
|
||||
}
|
||||
|
||||
func getReleaseByTag(owner, repo, tag string, client *gitea.Client) (*gitea.Release, error) {
|
||||
rl, _, err := client.ListReleases(owner, repo, gitea.ListReleasesOptions{
|
||||
ListOptions: gitea.ListOptions{Page: -1},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(rl) == 0 {
|
||||
return nil, fmt.Errorf("Repo does not have any release")
|
||||
}
|
||||
for _, r := range rl {
|
||||
if r.TagName == tag {
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("Release tag does not exist")
|
||||
}
|
||||
|
||||
35
cmd/releases/utils.go
Normal file
35
cmd/releases/utils.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package releases
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
)
|
||||
|
||||
// GetReleaseByTag finds a release by its tag name.
|
||||
func GetReleaseByTag(owner, repo, tag string, client *gitea.Client) (*gitea.Release, error) {
|
||||
for page := 1; ; {
|
||||
rl, resp, err := client.ListReleases(owner, repo, gitea.ListReleasesOptions{
|
||||
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if page == 1 && len(rl) == 0 {
|
||||
return nil, fmt.Errorf("repo does not have any release")
|
||||
}
|
||||
for _, r := range rl {
|
||||
if r.TagName == tag {
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
if resp == nil || resp.NextPage == 0 {
|
||||
break
|
||||
}
|
||||
page = resp.NextPage
|
||||
}
|
||||
return nil, fmt.Errorf("release tag does not exist")
|
||||
}
|
||||
@@ -15,13 +15,13 @@ import (
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
// CmdRepos represents to login a gitea server.
|
||||
// CmdRepos represents the command to manage repositories.
|
||||
var CmdRepos = cli.Command{
|
||||
Name: "repos",
|
||||
Aliases: []string{"repo"},
|
||||
Category: catEntities,
|
||||
Usage: "Show repository details",
|
||||
Description: "Show repository details",
|
||||
Usage: "Manage repositories",
|
||||
Description: "Manage repositories",
|
||||
ArgsUsage: "[<repo owner>/<repo name>]",
|
||||
Action: runRepos,
|
||||
Commands: []*cli.Command{
|
||||
|
||||
@@ -70,7 +70,7 @@ func runReposSearch(_ stdctx.Context, cmd *cli.Command) error {
|
||||
org, resp, err := client.GetOrg(teaCmd.String("owner"))
|
||||
if err != nil {
|
||||
if resp == nil || resp.StatusCode != http.StatusNotFound {
|
||||
return fmt.Errorf("Could not find owner: %w", err)
|
||||
return fmt.Errorf("could not find owner: %w", err)
|
||||
}
|
||||
|
||||
// if owner is no org, its a user
|
||||
|
||||
@@ -41,7 +41,7 @@ func runTrackedTimesAdd(_ stdctx.Context, cmd *cli.Command) error {
|
||||
}
|
||||
|
||||
if ctx.Args().Len() < 2 {
|
||||
return fmt.Errorf("No issue or duration specified.\nUsage:\t%s", ctx.Command.UsageText)
|
||||
return fmt.Errorf("no issue or duration specified.\nUsage:\t%s", ctx.Command.UsageText)
|
||||
}
|
||||
|
||||
issue, err := utils.ArgToIndex(ctx.Args().First())
|
||||
|
||||
@@ -36,7 +36,7 @@ func runTrackedTimesDelete(_ stdctx.Context, cmd *cli.Command) error {
|
||||
client := ctx.Login.Client()
|
||||
|
||||
if ctx.Args().Len() < 2 {
|
||||
return fmt.Errorf("No issue or time ID specified.\nUsage:\t%s", ctx.Command.UsageText)
|
||||
return fmt.Errorf("no issue or time ID specified.\nUsage:\t%s", ctx.Command.UsageText)
|
||||
}
|
||||
|
||||
issue, err := utils.ArgToIndex(ctx.Args().First())
|
||||
|
||||
@@ -35,7 +35,7 @@ func runTrackedTimesReset(_ stdctx.Context, cmd *cli.Command) error {
|
||||
client := ctx.Login.Client()
|
||||
|
||||
if ctx.Args().Len() != 1 {
|
||||
return fmt.Errorf("No issue specified.\nUsage:\t%s", ctx.Command.UsageText)
|
||||
return fmt.Errorf("no issue specified.\nUsage:\t%s", ctx.Command.UsageText)
|
||||
}
|
||||
|
||||
issue, err := utils.ArgToIndex(ctx.Args().First())
|
||||
|
||||
122
docs/CLI.md
122
docs/CLI.md
@@ -274,7 +274,7 @@ Manage and checkout pull requests
|
||||
**--comments**: Whether to display comments (will prompt if not provided & run interactively)
|
||||
|
||||
**--fields, -f**="": Comma-separated list of fields to print. Available values:
|
||||
index,state,author,author-id,url,title,body,mergeable,base,base-commit,head,diff,patch,created,updated,deadline,assignees,milestone,labels,comments
|
||||
index,state,author,author-id,url,title,body,mergeable,base,base-commit,head,diff,patch,created,updated,deadline,assignees,milestone,labels,comments,ci
|
||||
(default: "index,title,state,author,milestone,updated,labels")
|
||||
|
||||
**--limit, --lm**="": specify limit of items per page (default: 30)
|
||||
@@ -296,7 +296,7 @@ Manage and checkout pull requests
|
||||
List pull requests of the repository
|
||||
|
||||
**--fields, -f**="": Comma-separated list of fields to print. Available values:
|
||||
index,state,author,author-id,url,title,body,mergeable,base,base-commit,head,diff,patch,created,updated,deadline,assignees,milestone,labels,comments
|
||||
index,state,author,author-id,url,title,body,mergeable,base,base-commit,head,diff,patch,created,updated,deadline,assignees,milestone,labels,comments,ci
|
||||
(default: "index,title,state,author,milestone,updated,labels")
|
||||
|
||||
**--limit, --lm**="": specify limit of items per page (default: 30)
|
||||
@@ -483,6 +483,46 @@ Merge a pull request
|
||||
|
||||
**--title, -t**="": Merge commit title
|
||||
|
||||
### review-comments, rc
|
||||
|
||||
List review comments on a pull request
|
||||
|
||||
**--fields, -f**="": Comma-separated list of fields to print. Available values:
|
||||
id,body,reviewer,path,line,resolver,created,updated,url
|
||||
(default: "id,path,line,body,reviewer,resolver")
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||
|
||||
**--remote, -R**="": Discover Gitea login from remote. Optional
|
||||
|
||||
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
|
||||
|
||||
### resolve
|
||||
|
||||
Resolve a review comment on a pull request
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||
|
||||
**--remote, -R**="": Discover Gitea login from remote. Optional
|
||||
|
||||
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
|
||||
|
||||
### unresolve
|
||||
|
||||
Unresolve a review comment on a pull request
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||
|
||||
**--remote, -R**="": Discover Gitea login from remote. Optional
|
||||
|
||||
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
|
||||
|
||||
## labels, label
|
||||
|
||||
Manage issue labels
|
||||
@@ -1025,7 +1065,7 @@ Delete users Organizations
|
||||
|
||||
## repos, repo
|
||||
|
||||
Show repository details
|
||||
Manage repositories
|
||||
|
||||
**--fields, -f**="": Comma-separated list of fields to print. Available values:
|
||||
description,forks,id,name,owner,stars,ssh,updated,url,permission,type
|
||||
@@ -1341,6 +1381,26 @@ Unprotect branches
|
||||
|
||||
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
|
||||
|
||||
### rename, rn
|
||||
|
||||
Rename a branch
|
||||
|
||||
**--fields, -f**="": Comma-separated list of fields to print. Available values:
|
||||
name,protected,user-can-merge,user-can-push,protection
|
||||
(default: "name,protected,user-can-merge,user-can-push")
|
||||
|
||||
**--limit, --lm**="": specify limit of items per page (default: 30)
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
**--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
|
||||
|
||||
## actions, action
|
||||
|
||||
Manage repository actions
|
||||
@@ -1533,13 +1593,65 @@ Manage repository workflows
|
||||
|
||||
List repository workflows
|
||||
|
||||
**--limit, --lm**="": specify limit of items per page (default: 30)
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||
|
||||
**--remote, -R**="": Discover Gitea login from remote. Optional
|
||||
|
||||
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
|
||||
|
||||
#### view, show, get
|
||||
|
||||
View workflow details
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
**--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
|
||||
|
||||
#### dispatch, trigger, run
|
||||
|
||||
Dispatch a workflow run
|
||||
|
||||
**--follow, -f**: follow log output after dispatching
|
||||
|
||||
**--input, -i**="": workflow input in key=value format (can be specified multiple times)
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||
|
||||
**--ref, -r**="": branch or tag to dispatch on (default: current branch)
|
||||
|
||||
**--remote, -R**="": Discover Gitea login from remote. Optional
|
||||
|
||||
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
|
||||
|
||||
#### enable
|
||||
|
||||
Enable a workflow
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||
|
||||
**--remote, -R**="": Discover Gitea login from remote. Optional
|
||||
|
||||
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
|
||||
|
||||
#### disable
|
||||
|
||||
Disable a workflow
|
||||
|
||||
**--confirm, -y**: confirm disable without prompting
|
||||
|
||||
**--login, -l**="": Use a different Gitea Login. Optional
|
||||
|
||||
**--output, -o**="": Output format. (simple, table, csv, tsv, yaml, json)
|
||||
|
||||
**--remote, -R**="": Discover Gitea login from remote. Optional
|
||||
|
||||
|
||||
@@ -1,8 +1,93 @@
|
||||
# Gitea actions workflows
|
||||
|
||||
## Workflow management with tea
|
||||
|
||||
### List workflows
|
||||
|
||||
```bash
|
||||
# List all workflows in the repository
|
||||
tea actions workflows list
|
||||
```
|
||||
|
||||
### View workflow details
|
||||
|
||||
```bash
|
||||
# View details of a specific workflow by ID or filename
|
||||
tea actions workflows view deploy.yml
|
||||
```
|
||||
|
||||
### Dispatch (trigger) a workflow
|
||||
|
||||
```bash
|
||||
# Dispatch a workflow on the current branch
|
||||
tea actions workflows dispatch deploy.yml
|
||||
|
||||
# Dispatch on a specific branch
|
||||
tea actions workflows dispatch deploy.yml --ref main
|
||||
|
||||
# Dispatch with workflow inputs
|
||||
tea actions workflows dispatch deploy.yml --ref main --input env=staging --input version=1.2.3
|
||||
|
||||
# Dispatch and follow log output
|
||||
tea actions workflows dispatch ci.yml --ref feature/my-pr --follow
|
||||
```
|
||||
|
||||
### Enable / disable workflows
|
||||
|
||||
```bash
|
||||
# Disable a workflow
|
||||
tea actions workflows disable deploy.yml --confirm
|
||||
|
||||
# Enable a workflow
|
||||
tea actions workflows enable deploy.yml
|
||||
```
|
||||
|
||||
## Example: Re-trigger CI from an AI-driven PR flow
|
||||
|
||||
Use `tea actions workflows dispatch` to re-run a specific workflow after
|
||||
pushing changes in an automated PR workflow:
|
||||
|
||||
```bash
|
||||
# Push changes to a feature branch, then re-trigger CI
|
||||
git push origin feature/auto-fix
|
||||
tea actions workflows dispatch check-and-test --ref feature/auto-fix --follow
|
||||
```
|
||||
|
||||
## Example: Dispatch a workflow with `workflow_dispatch` trigger
|
||||
|
||||
```yaml
|
||||
name: deploy
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
env:
|
||||
description: "Target environment"
|
||||
required: true
|
||||
default: "staging"
|
||||
version:
|
||||
description: "Version to deploy"
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Deploy
|
||||
run: |
|
||||
echo "Deploying version ${{ gitea.event.inputs.version }} to ${{ gitea.event.inputs.env }}"
|
||||
```
|
||||
|
||||
Trigger this workflow from the CLI:
|
||||
|
||||
```bash
|
||||
tea actions workflows dispatch deploy.yml --ref main --input env=production --input version=2.0.0
|
||||
```
|
||||
|
||||
## Merge Pull request on approval
|
||||
|
||||
``` Yaml
|
||||
```yaml
|
||||
---
|
||||
name: Pull request
|
||||
on:
|
||||
|
||||
45
go.mod
45
go.mod
@@ -5,7 +5,7 @@ go 1.26
|
||||
require (
|
||||
charm.land/glamour/v2 v2.0.0
|
||||
charm.land/huh/v2 v2.0.3
|
||||
charm.land/lipgloss/v2 v2.0.2
|
||||
charm.land/lipgloss/v2 v2.0.3
|
||||
code.gitea.io/gitea-vet v0.2.3
|
||||
code.gitea.io/sdk/gitea v0.24.1
|
||||
gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c
|
||||
@@ -13,28 +13,27 @@ require (
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
||||
github.com/enescakir/emoji v1.0.0
|
||||
github.com/go-authgate/sdk-go v0.6.1
|
||||
github.com/go-git/go-git/v5 v5.17.2
|
||||
github.com/go-git/go-git/v5 v5.18.0
|
||||
github.com/muesli/termenv v0.16.0
|
||||
github.com/olekukonko/tablewriter v1.1.4
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/urfave/cli-docs/v3 v3.1.0
|
||||
github.com/urfave/cli/v3 v3.8.0
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sys v0.42.0
|
||||
golang.org/x/term v0.41.0
|
||||
golang.org/x/sys v0.43.0
|
||||
golang.org/x/term v0.42.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
al.essio.dev/pkg/shellescape v1.6.0 // indirect
|
||||
charm.land/bubbles/v2 v2.0.0 // indirect
|
||||
charm.land/bubbles/v2 v2.1.0 // indirect
|
||||
charm.land/bubbletea/v2 v2.0.2 // indirect
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/42wim/httpsig v1.2.4 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.4.0 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.4.1 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.23.1 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
@@ -42,10 +41,10 @@ require (
|
||||
github.com/catppuccin/go v0.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260309091805-903bfd0cf188 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260330092749-0f94982c930b // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.7 // indirect
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20260311145557-c83711a11ffa // indirect
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20260406091427-a791e22d5143 // indirect
|
||||
github.com/charmbracelet/x/exp/strings v0.1.0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||
@@ -61,28 +60,28 @@ require (
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/go-fed/httpsig v1.1.0 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.8.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/hashicorp/go-version v1.8.0 // indirect
|
||||
github.com/hashicorp/go-version v1.9.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.21 // indirect
|
||||
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.2.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.7 // indirect
|
||||
github.com/olekukonko/ll v0.1.8 // indirect
|
||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
@@ -91,13 +90,13 @@ require (
|
||||
github.com/skeema/knownhosts v1.3.2 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yuin/goldmark v1.7.16 // indirect
|
||||
github.com/yuin/goldmark v1.8.2 // indirect
|
||||
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
||||
github.com/zalando/go-keyring v0.2.6 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
github.com/zalando/go-keyring v0.2.8 // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
||||
|
||||
105
go.sum
105
go.sum
@@ -1,7 +1,5 @@
|
||||
al.essio.dev/pkg/shellescape v1.6.0 h1:NxFcEqzFSEVCGN2yq7Huv/9hyCEGVa/TncnOOBBeXHA=
|
||||
al.essio.dev/pkg/shellescape v1.6.0/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
|
||||
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
|
||||
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
|
||||
charm.land/bubbles/v2 v2.1.0 h1:YSnNh5cPYlYjPxRrzs5VEn3vwhtEn3jVGRBT3M7/I0g=
|
||||
charm.land/bubbles/v2 v2.1.0/go.mod h1:l97h4hym2hvWBVfmJDtrEHHCtkIKeTEb3TTJ4ZOB3wY=
|
||||
charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0=
|
||||
charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
|
||||
charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U=
|
||||
@@ -10,18 +8,16 @@ charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU=
|
||||
charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc=
|
||||
charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs=
|
||||
charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM=
|
||||
charm.land/lipgloss/v2 v2.0.3 h1:yM2zJ4Cf5Y51b7RHIwioil4ApI/aypFXXVHSwlM6RzU=
|
||||
charm.land/lipgloss/v2 v2.0.3/go.mod h1:7myLU9iG/3xluAWzpY/fSxYYHCgoKTie7laxk6ATwXA=
|
||||
code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI=
|
||||
code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
|
||||
code.gitea.io/sdk/gitea v0.23.2 h1:iJB1FDmLegwfwjX8gotBDHdPSbk/ZR8V9VmEJaVsJYg=
|
||||
code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
|
||||
code.gitea.io/sdk/gitea v0.24.1 h1:hpaqcdGcBmfMpV7JSbBJVwE99qo+WqGreJYKrDKEyW8=
|
||||
code.gitea.io/sdk/gitea v0.24.1/go.mod h1:5/77BL3sHneCMEiZaMT9lfTvnnibsYxyO48mceCF3qA=
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c h1:8fTkq2UaVkLHZCF+iB4wTxINmVAToe2geZGayk9LMbA=
|
||||
gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c/go.mod h1:Fc8iyPm4NINRWujeIk2bTfcbGc4ZYY29/oMAAGcr4qI=
|
||||
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
|
||||
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
|
||||
github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU=
|
||||
github.com/42wim/httpsig v1.2.4/go.mod h1:yKsYfSyTBEohkPik224QPFylmzEBtda/kjyIAJjh3ps=
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
@@ -29,8 +25,8 @@ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProtonMail/go-crypto v1.4.0 h1:Zq/pbM3F5DFgJiMouxEdSVY44MVoQNEKp5d5QxIQceQ=
|
||||
github.com/ProtonMail/go-crypto v1.4.0/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
|
||||
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
|
||||
github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
|
||||
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
|
||||
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
@@ -59,10 +55,12 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260309091805-903bfd0cf188 h1:J8v4kWJYCaxv1SLhLunN74S+jMteZ1f7Dae99ioq4Bo=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260309091805-903bfd0cf188/go.mod h1:FzWNAbe1jEmI+GZljSnlaSA8wJjnNIZhWBLkTsAl6eg=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260330092749-0f94982c930b h1:ASDO9RT6SNKTQN87jO2bRfxHFJq8cgeYdFzivY2gCeM=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20260330092749-0f94982c930b/go.mod h1:Vo8TffMf0q7Uho/n8e6XpBZvOWtd3g39yX+9P5rRutA=
|
||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
||||
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
||||
github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4jxsAuEs=
|
||||
github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk=
|
||||
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
||||
@@ -71,8 +69,8 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6g
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE=
|
||||
github.com/charmbracelet/x/exp/ordered v0.1.0/go.mod h1:5UHwmG+is5THxMyCJHNPCn2/ecI07aKNrW+LcResjJ8=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20260311145557-c83711a11ffa h1:bmNUSF4m+fwrzZAOhluMSZxdM4bk+SWN0Ni2DimCZm8=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20260311145557-c83711a11ffa/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20260406091427-a791e22d5143 h1:aEppolah2k9c0LzKX2fk5ryuyQ0Lq8kCOjkvMw1b8o4=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20260406091427-a791e22d5143/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
|
||||
github.com/charmbracelet/x/exp/strings v0.1.0 h1:i69S2XI7uG1u4NLGeJPSYU++Nmjvpo9nwd6aoEm7gkA=
|
||||
github.com/charmbracelet/x/exp/strings v0.1.0/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
@@ -112,12 +110,10 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/enescakir/emoji v1.0.0 h1:W+HsNql8swfCQFtioDGDHCHri8nudlK1n5p2rHCJoog=
|
||||
github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkKp+WKFD0=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-authgate/sdk-go v0.2.0 h1:w22f+sAg/YMqnLOcS/4SAuMZXTbPurzkSQBsjb1hcbw=
|
||||
github.com/go-authgate/sdk-go v0.2.0/go.mod h1:RGqvrFdrPnOumndoQQV8qzu8zP1KFUZPdhX0IkWduho=
|
||||
github.com/go-authgate/sdk-go v0.6.1 h1:oQREINU63YckTRdJ+0VBmN6ewFSMXa0D862w8624/jw=
|
||||
github.com/go-authgate/sdk-go v0.6.1/go.mod h1:55PLAPuu8GDK0omOwG6lx4c+9/T6dJwZd8kecUueLEk=
|
||||
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||
@@ -128,26 +124,22 @@ github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDz
|
||||
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM=
|
||||
github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI=
|
||||
github.com/go-git/go-git/v5 v5.17.1 h1:WnljyxIzSj9BRRUlnmAU35ohDsjRK0EKmL0evDqi5Jk=
|
||||
github.com/go-git/go-git/v5 v5.17.1/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
|
||||
github.com/go-git/go-git/v5 v5.17.2 h1:B+nkdlxdYrvyFK4GPXVU8w1U+YkbsgciIR7f2sZJ104=
|
||||
github.com/go-git/go-git/v5 v5.17.2/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/go-git/go-git/v5 v5.18.0 h1:O831KI+0PR51hM2kep6T8k+w0/LIAD490gvqMCvL5hM=
|
||||
github.com/go-git/go-git/v5 v5.18.0/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
||||
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
|
||||
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
@@ -163,15 +155,15 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
||||
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
@@ -184,10 +176,8 @@ github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
||||
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.1.7 h1:WyK1YZwOTUKHEXZz3VydBDT5t3zDqa9yI8iJg5PHon4=
|
||||
github.com/olekukonko/ll v0.1.7/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
|
||||
github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA=
|
||||
github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM=
|
||||
github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
|
||||
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
|
||||
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
||||
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
@@ -225,8 +215,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw=
|
||||
github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to=
|
||||
github.com/urfave/cli/v3 v3.7.0 h1:AGSnbUyjtLiM+WJUb4dzXKldl/gL+F8OwmRDtVr6g2U=
|
||||
github.com/urfave/cli/v3 v3.7.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
|
||||
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
@@ -234,31 +222,31 @@ github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
||||
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
|
||||
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
|
||||
github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPccHs=
|
||||
github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -273,22 +261,21 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -226,7 +226,6 @@ func startLocalServerAndOpenBrowser(authURL, expectedState string, opts OAuthOpt
|
||||
codeChan := make(chan string, 1)
|
||||
stateChan := make(chan string, 1)
|
||||
errChan := make(chan error, 1)
|
||||
portChan := make(chan int, 1)
|
||||
|
||||
// Parse the redirect URL to get the path
|
||||
parsedURL, err := url.Parse(opts.RedirectURL)
|
||||
@@ -311,7 +310,6 @@ func startLocalServerAndOpenBrowser(authURL, expectedState string, opts OAuthOpt
|
||||
if port == 0 {
|
||||
addr := listener.Addr().(*net.TCPAddr)
|
||||
port = addr.Port
|
||||
portChan <- port
|
||||
|
||||
// Update redirect URL with actual port
|
||||
parsedURL.Host = fmt.Sprintf("%s:%d", hostname, port)
|
||||
|
||||
@@ -5,7 +5,6 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
@@ -74,7 +73,8 @@ func GetConfigPath() string {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("unable to get or create config file")
|
||||
fmt.Fprintln(os.Stderr, "unable to get or create config file")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return configFilePath
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
@@ -132,7 +131,7 @@ func GetDefaultLogin() (*Login, error) {
|
||||
}
|
||||
|
||||
if len(config.Logins) == 0 {
|
||||
return nil, errors.New("No available login")
|
||||
return nil, errors.New("no available login")
|
||||
}
|
||||
for _, l := range config.Logins {
|
||||
if l.Default {
|
||||
@@ -178,50 +177,51 @@ func GetLoginByName(name string) (*Login, error) {
|
||||
}
|
||||
|
||||
// GetLoginByToken get login by token
|
||||
func GetLoginByToken(token string) *Login {
|
||||
func GetLoginByToken(token string) (*Login, error) {
|
||||
if token == "" {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
err := loadConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
if err := loadConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, l := range config.Logins {
|
||||
if l.Token == token {
|
||||
return &l
|
||||
return &l, nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetLoginByHost finds a login by it's server URL
|
||||
func GetLoginByHost(host string) *Login {
|
||||
logins := GetLoginsByHost(host)
|
||||
if len(logins) > 0 {
|
||||
return logins[0]
|
||||
// GetLoginByHost finds a login by its server URL
|
||||
func GetLoginByHost(host string) (*Login, error) {
|
||||
logins, err := GetLoginsByHost(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil
|
||||
if len(logins) > 0 {
|
||||
return logins[0], nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetLoginsByHost returns all logins matching a host
|
||||
func GetLoginsByHost(host string) []*Login {
|
||||
err := loadConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
func GetLoginsByHost(host string) ([]*Login, error) {
|
||||
if err := loadConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var matches []*Login
|
||||
for i := range config.Logins {
|
||||
loginURL, err := url.Parse(config.Logins[i].URL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return nil, err
|
||||
}
|
||||
if loginURL.Host == host {
|
||||
matches = append(matches, &config.Logins[i])
|
||||
}
|
||||
}
|
||||
return matches
|
||||
return matches, nil
|
||||
}
|
||||
|
||||
// DeleteLogin delete a login by name from config
|
||||
@@ -417,12 +417,13 @@ func doOAuthRefresh(l *Login) (*oauth2.Token, error) {
|
||||
func (l *Login) Client(options ...gitea.ClientOption) *gitea.Client {
|
||||
// Refresh OAuth token if expired or near expiry
|
||||
if err := l.RefreshOAuthTokenIfNeeded(); err != nil {
|
||||
log.Fatalf("Failed to refresh token: %s\nPlease use 'tea login oauth-refresh %s' to manually refresh the token.\n", err, l.Name)
|
||||
fmt.Fprintf(os.Stderr, "Failed to refresh token: %s\nPlease use 'tea login oauth-refresh %s' to manually refresh the token.\n", err, l.Name)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
httpClient := &http.Client{}
|
||||
if l.Insecure {
|
||||
cookieJar, _ := cookiejar.New(nil)
|
||||
cookieJar, _ := cookiejar.New(nil) // New with nil options never returns an error
|
||||
|
||||
httpClient = &http.Client{
|
||||
Jar: cookieJar,
|
||||
@@ -443,12 +444,18 @@ func (l *Login) Client(options ...gitea.ClientOption) *gitea.Client {
|
||||
}
|
||||
|
||||
if l.SSHCertPrincipal != "" {
|
||||
l.askForSSHPassphrase()
|
||||
if err := l.askForSSHPassphrase(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to read SSH passphrase: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
options = append(options, gitea.UseSSHCert(l.SSHCertPrincipal, l.SSHKey, l.SSHPassphrase))
|
||||
}
|
||||
|
||||
if l.SSHKeyFingerprint != "" {
|
||||
l.askForSSHPassphrase()
|
||||
if err := l.askForSSHPassphrase(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to read SSH passphrase: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
options = append(options, gitea.UseSSHPubkey(l.SSHKeyFingerprint, l.SSHKey, l.SSHPassphrase))
|
||||
}
|
||||
|
||||
@@ -456,25 +463,25 @@ func (l *Login) Client(options ...gitea.ClientOption) *gitea.Client {
|
||||
if err != nil {
|
||||
var versionError *gitea.ErrUnknownVersion
|
||||
if !errors.As(err, &versionError) {
|
||||
log.Fatal(err)
|
||||
fmt.Fprintf(os.Stderr, "Failed to create Gitea client: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "WARNING: could not detect gitea version: %s\nINFO: set gitea version: to last supported one\n", versionError)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func (l *Login) askForSSHPassphrase() {
|
||||
func (l *Login) askForSSHPassphrase() error {
|
||||
if ok, err := utils.IsKeyEncrypted(l.SSHKey); ok && err == nil && l.SSHPassphrase == "" {
|
||||
if err := huh.NewInput().
|
||||
return huh.NewInput().
|
||||
Title("ssh-key is encrypted please enter the passphrase: ").
|
||||
Validate(huh.ValidateNotEmpty()).
|
||||
EchoMode(huh.EchoModePassword).
|
||||
Value(&l.SSHPassphrase).
|
||||
WithTheme(theme.GetTheme()).
|
||||
Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
Run()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSSHHost returns SSH host name
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
var errNotAGiteaRepo = errors.New("No Gitea login found. You might want to specify --repo (and --login) to work outside of a repository")
|
||||
var errNotAGiteaRepo = errors.New("no Gitea login found; you might want to specify --repo (and --login) to work outside of a repository")
|
||||
|
||||
// ErrCommandCanceled is returned when the user explicitly cancels an interactive prompt.
|
||||
var ErrCommandCanceled = errors.New("command canceled")
|
||||
@@ -83,6 +83,8 @@ func InitCommand(cmd *cli.Command) (*TeaContext, error) {
|
||||
}
|
||||
if repoFlagPathExists {
|
||||
repoPath = repoFlag
|
||||
} else {
|
||||
c.RepoSlug = repoFlag
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,12 +92,6 @@ func InitCommand(cmd *cli.Command) (*TeaContext, error) {
|
||||
remoteFlag = config.GetPreferences().FlagDefaults.Remote
|
||||
}
|
||||
|
||||
if repoPath == "" {
|
||||
if repoPath, err = os.Getwd(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Create env login before repo context detection so it participates in remote URL matching
|
||||
var extraLogins []config.Login
|
||||
envLogin := GetLoginByEnvVar()
|
||||
@@ -108,6 +104,13 @@ func InitCommand(cmd *cli.Command) (*TeaContext, error) {
|
||||
|
||||
// try to read local git repo & extract context: if repoFlag specifies a valid path, read repo in that dir,
|
||||
// otherwise attempt PWD. if no repo is found, continue with default login
|
||||
if c.RepoSlug == "" {
|
||||
if repoPath == "" {
|
||||
if repoPath, err = os.Getwd(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if c.LocalRepo, c.Login, c.RepoSlug, err = contextFromLocalRepo(repoPath, remoteFlag, extraLogins); err != nil {
|
||||
if err == errNotAGiteaRepo || err == gogit.ErrRepositoryNotExists {
|
||||
// we can deal with that, commands needing the optional values use ctx.Ensure()
|
||||
@@ -115,10 +118,6 @@ func InitCommand(cmd *cli.Command) (*TeaContext, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(repoFlag) != 0 && !repoFlagPathExists {
|
||||
// if repoFlag is not a valid path, use it to override repoSlug
|
||||
c.RepoSlug = repoFlag
|
||||
}
|
||||
|
||||
// If env vars are set, always use the env login (but repo slug was already
|
||||
|
||||
@@ -4,9 +4,14 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/tea/modules/config"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
func Test_MatchLogins(t *testing.T) {
|
||||
@@ -65,3 +70,47 @@ func Test_MatchLogins(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitCommand_WithRepoSlugSkipsLocalRepoDetection(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
config.SetConfigForTesting(config.LocalConfig{
|
||||
Logins: []config.Login{{
|
||||
Name: "test-login",
|
||||
URL: "https://gitea.example.com",
|
||||
Token: "token",
|
||||
User: "login-user",
|
||||
Default: true,
|
||||
}},
|
||||
})
|
||||
|
||||
cmd := exec.Command("git", "init", "--object-format=sha256", tmpDir)
|
||||
cmd.Env = os.Environ()
|
||||
require.NoError(t, cmd.Run())
|
||||
|
||||
oldWd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.Chdir(tmpDir))
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, os.Chdir(oldWd))
|
||||
})
|
||||
|
||||
cliCmd := cli.Command{
|
||||
Name: "branches",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{Name: "login"},
|
||||
&cli.StringFlag{Name: "repo"},
|
||||
&cli.StringFlag{Name: "remote"},
|
||||
&cli.StringFlag{Name: "output"},
|
||||
},
|
||||
}
|
||||
require.NoError(t, cliCmd.Set("repo", "owner/repo"))
|
||||
|
||||
ctx, err := InitCommand(&cliCmd)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "owner", ctx.Owner)
|
||||
require.Equal(t, "repo", ctx.Repo)
|
||||
require.Equal(t, "owner/repo", ctx.RepoSlug)
|
||||
require.Nil(t, ctx.LocalRepo)
|
||||
require.NotNil(t, ctx.Login)
|
||||
require.Equal(t, "test-login", ctx.Login.Name)
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func (r TeaRepo) TeaFindBranchBySha(sha, repoURL string) (b *git_config.Branch,
|
||||
return nil, err
|
||||
}
|
||||
if remote == nil {
|
||||
return nil, fmt.Errorf("No remote found for '%s'", repoURL)
|
||||
return nil, fmt.Errorf("no remote found for '%s'", repoURL)
|
||||
}
|
||||
remoteName := remote.Config().Name
|
||||
|
||||
@@ -133,7 +133,7 @@ func (r TeaRepo) TeaFindBranchByName(branchName, repoURL string) (b *git_config.
|
||||
return nil, err
|
||||
}
|
||||
if remote == nil {
|
||||
return nil, fmt.Errorf("No remote found for '%s'", repoURL)
|
||||
return nil, fmt.Errorf("no remote found for '%s'", repoURL)
|
||||
}
|
||||
remoteName := remote.Config().Name
|
||||
|
||||
|
||||
@@ -180,19 +180,25 @@ func fetchIssueSelectables(login *config.Login, owner, repo string, done chan is
|
||||
r.MilestoneList[i] = m.Title
|
||||
}
|
||||
|
||||
labels, _, err := c.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: -1},
|
||||
r.LabelMap = make(map[string]int64)
|
||||
r.LabelList = make([]string, 0)
|
||||
for page := 1; ; {
|
||||
labels, resp, err := c.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||
})
|
||||
if err != nil {
|
||||
r.Err = err
|
||||
done <- r
|
||||
return
|
||||
}
|
||||
r.LabelMap = make(map[string]int64)
|
||||
r.LabelList = make([]string, len(labels))
|
||||
for i, l := range labels {
|
||||
for _, l := range labels {
|
||||
r.LabelMap[l.Name] = l.ID
|
||||
r.LabelList[i] = l.Name
|
||||
r.LabelList = append(r.LabelList, l.Name)
|
||||
}
|
||||
if resp == nil || resp.NextPage == 0 {
|
||||
break
|
||||
}
|
||||
page = resp.NextPage
|
||||
}
|
||||
|
||||
done <- r
|
||||
|
||||
@@ -41,7 +41,7 @@ func CreateLogin() error {
|
||||
}
|
||||
_, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid URL: %v", err)
|
||||
return fmt.Errorf("invalid URL: %v", err)
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
@@ -69,7 +69,7 @@ func CreateLogin() error {
|
||||
}
|
||||
for _, login := range logins {
|
||||
if login.Name == name {
|
||||
return fmt.Errorf("Login with name '%s' already exists", name)
|
||||
return fmt.Errorf("login with name '%s' already exists", name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -154,7 +154,7 @@ func CreateLogin() error {
|
||||
Value(&tokenScopes).
|
||||
Validate(func(s []string) error {
|
||||
if len(s) == 0 {
|
||||
return errors.New("At least one scope is required")
|
||||
return errors.New("at least one scope is required")
|
||||
}
|
||||
return nil
|
||||
}).
|
||||
|
||||
@@ -58,7 +58,7 @@ func getPullIndex(ctx *context.TeaContext, branch string) (int64, error) {
|
||||
return 0, err
|
||||
}
|
||||
if len(prs) == 0 {
|
||||
return 0, fmt.Errorf("No open PRs found")
|
||||
return 0, fmt.Errorf("no open PRs found")
|
||||
}
|
||||
opts.ListOptions.Page++
|
||||
prOptions := make([]string, 0)
|
||||
|
||||
@@ -154,27 +154,23 @@ func ActionWorkflowJobsList(jobs []*gitea.ActionWorkflowJob, output string) erro
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// WorkflowsList prints a list of workflow files with active status
|
||||
func WorkflowsList(workflows []*gitea.ContentsResponse, activeStatus map[string]bool, output string) error {
|
||||
// ActionWorkflowsList prints a list of workflows from the workflow API
|
||||
func ActionWorkflowsList(workflows []*gitea.ActionWorkflow, output string) error {
|
||||
t := table{
|
||||
headers: []string{
|
||||
"Active",
|
||||
"ID",
|
||||
"Name",
|
||||
"Path",
|
||||
"State",
|
||||
},
|
||||
}
|
||||
|
||||
machineReadable := isMachineReadable(output)
|
||||
|
||||
for _, workflow := range workflows {
|
||||
// Check if this workflow file is active (has runs)
|
||||
isActive := activeStatus[workflow.Name]
|
||||
activeIndicator := formatBoolean(isActive, !machineReadable)
|
||||
|
||||
for _, wf := range workflows {
|
||||
t.addRow(
|
||||
activeIndicator,
|
||||
workflow.Name,
|
||||
workflow.Path,
|
||||
wf.ID,
|
||||
wf.Name,
|
||||
wf.Path,
|
||||
wf.State,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -186,3 +182,34 @@ func WorkflowsList(workflows []*gitea.ContentsResponse, activeStatus map[string]
|
||||
t.sort(1, true) // Sort by name column
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// ActionWorkflowDetails prints detailed information about a workflow
|
||||
func ActionWorkflowDetails(wf *gitea.ActionWorkflow) {
|
||||
fmt.Printf("ID: %s\n", wf.ID)
|
||||
fmt.Printf("Name: %s\n", wf.Name)
|
||||
fmt.Printf("Path: %s\n", wf.Path)
|
||||
fmt.Printf("State: %s\n", wf.State)
|
||||
if wf.HTMLURL != "" {
|
||||
fmt.Printf("URL: %s\n", wf.HTMLURL)
|
||||
}
|
||||
if wf.BadgeURL != "" {
|
||||
fmt.Printf("Badge: %s\n", wf.BadgeURL)
|
||||
}
|
||||
if !wf.CreatedAt.IsZero() {
|
||||
fmt.Printf("Created: %s\n", FormatTime(wf.CreatedAt, false))
|
||||
}
|
||||
if !wf.UpdatedAt.IsZero() {
|
||||
fmt.Printf("Updated: %s\n", FormatTime(wf.UpdatedAt, false))
|
||||
}
|
||||
}
|
||||
|
||||
// ActionWorkflowDispatchResult prints the result of a workflow dispatch
|
||||
func ActionWorkflowDispatchResult(details *gitea.RunDetails) {
|
||||
fmt.Printf("Workflow dispatched successfully\n")
|
||||
if details != nil {
|
||||
fmt.Printf("Run ID: %d\n", details.WorkflowRunID)
|
||||
if details.HTMLURL != "" {
|
||||
fmt.Printf("URL: %s\n", details.HTMLURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,87 @@ func TestActionWorkflowJobsListWithData(t *testing.T) {
|
||||
require.NoError(t, ActionWorkflowJobsList(jobs, ""))
|
||||
}
|
||||
|
||||
func TestActionWorkflowsListEmpty(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionWorkflowsList panicked with empty list: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
require.NoError(t, ActionWorkflowsList([]*gitea.ActionWorkflow{}, ""))
|
||||
}
|
||||
|
||||
func TestActionWorkflowsListWithData(t *testing.T) {
|
||||
workflows := []*gitea.ActionWorkflow{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "CI",
|
||||
Path: ".gitea/workflows/ci.yml",
|
||||
State: "active",
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Name: "Deploy",
|
||||
Path: ".gitea/workflows/deploy.yml",
|
||||
State: "disabled_manually",
|
||||
},
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionWorkflowsList panicked with data: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
require.NoError(t, ActionWorkflowsList(workflows, ""))
|
||||
}
|
||||
|
||||
func TestActionWorkflowDetails(t *testing.T) {
|
||||
wf := &gitea.ActionWorkflow{
|
||||
ID: "1",
|
||||
Name: "CI Pipeline",
|
||||
Path: ".gitea/workflows/ci.yml",
|
||||
State: "active",
|
||||
HTMLURL: "https://gitea.example.com/owner/repo/actions/workflows/ci.yml",
|
||||
BadgeURL: "https://gitea.example.com/owner/repo/actions/workflows/ci.yml/badge.svg",
|
||||
CreatedAt: time.Now().Add(-24 * time.Hour),
|
||||
UpdatedAt: time.Now().Add(-1 * time.Hour),
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionWorkflowDetails panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ActionWorkflowDetails(wf)
|
||||
}
|
||||
|
||||
func TestActionWorkflowDispatchResult(t *testing.T) {
|
||||
details := &gitea.RunDetails{
|
||||
WorkflowRunID: 42,
|
||||
HTMLURL: "https://gitea.example.com/owner/repo/actions/runs/42",
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionWorkflowDispatchResult panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ActionWorkflowDispatchResult(details)
|
||||
}
|
||||
|
||||
func TestActionWorkflowDispatchResultNil(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionWorkflowDispatchResult panicked with nil: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ActionWorkflowDispatchResult(nil)
|
||||
}
|
||||
|
||||
func TestFormatDurationMinutes(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
var ciStatusSymbols = map[gitea.StatusState]string{
|
||||
gitea.StatusSuccess: "✓ ",
|
||||
gitea.StatusPending: "⭮ ",
|
||||
gitea.StatusPending: "⏳ ",
|
||||
gitea.StatusWarning: "⚠ ",
|
||||
gitea.StatusError: "✘ ",
|
||||
gitea.StatusFailure: "❌ ",
|
||||
@@ -42,16 +42,19 @@ func PullDetails(pr *gitea.PullRequest, reviews []*gitea.PullReview, ciStatus *g
|
||||
|
||||
out += formatReviews(pr, reviews)
|
||||
|
||||
if ciStatus != nil {
|
||||
var summary, errors string
|
||||
if ciStatus != nil && len(ciStatus.Statuses) != 0 {
|
||||
out += "- CI:\n"
|
||||
for _, s := range ciStatus.Statuses {
|
||||
summary += ciStatusSymbols[s.State]
|
||||
if s.State != gitea.StatusSuccess {
|
||||
errors += fmt.Sprintf(" - [**%s**:\t%s](%s)\n", s.Context, s.Description, s.TargetURL)
|
||||
symbol := ciStatusSymbols[s.State]
|
||||
if s.TargetURL != "" {
|
||||
out += fmt.Sprintf(" - %s[**%s**](%s)", symbol, s.Context, s.TargetURL)
|
||||
} else {
|
||||
out += fmt.Sprintf(" - %s**%s**", symbol, s.Context)
|
||||
}
|
||||
if s.Description != "" {
|
||||
out += fmt.Sprintf(": %s", s.Description)
|
||||
}
|
||||
if len(ciStatus.Statuses) != 0 {
|
||||
out += fmt.Sprintf("- CI: %s\n%s", summary, errors)
|
||||
out += "\n"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +92,20 @@ func formatPRState(pr *gitea.PullRequest) string {
|
||||
return string(pr.State)
|
||||
}
|
||||
|
||||
func formatCIStatus(ci *gitea.CombinedStatus, machineReadable bool) string {
|
||||
if ci == nil || len(ci.Statuses) == 0 {
|
||||
return ""
|
||||
}
|
||||
if machineReadable {
|
||||
return string(ci.State)
|
||||
}
|
||||
items := make([]string, 0, len(ci.Statuses))
|
||||
for _, s := range ci.Statuses {
|
||||
items = append(items, fmt.Sprintf("%s%s", ciStatusSymbols[s.State], s.Context))
|
||||
}
|
||||
return strings.Join(items, ", ")
|
||||
}
|
||||
|
||||
func formatReviews(pr *gitea.PullRequest, reviews []*gitea.PullReview) string {
|
||||
result := ""
|
||||
if len(reviews) == 0 {
|
||||
@@ -138,8 +155,8 @@ func formatReviews(pr *gitea.PullRequest, reviews []*gitea.PullReview) string {
|
||||
}
|
||||
|
||||
// PullsList prints a listing of pulls
|
||||
func PullsList(prs []*gitea.PullRequest, output string, fields []string) error {
|
||||
return printPulls(prs, output, fields)
|
||||
func PullsList(prs []*gitea.PullRequest, output string, fields []string, ciStatuses map[int64]*gitea.CombinedStatus) error {
|
||||
return printPulls(prs, output, fields, ciStatuses)
|
||||
}
|
||||
|
||||
// PullFields are all available fields to print with PullsList()
|
||||
@@ -168,9 +185,10 @@ var PullFields = []string{
|
||||
"milestone",
|
||||
"labels",
|
||||
"comments",
|
||||
"ci",
|
||||
}
|
||||
|
||||
func printPulls(pulls []*gitea.PullRequest, output string, fields []string) error {
|
||||
func printPulls(pulls []*gitea.PullRequest, output string, fields []string, ciStatuses map[int64]*gitea.CombinedStatus) error {
|
||||
labelMap := map[int64]string{}
|
||||
printables := make([]printable, len(pulls))
|
||||
machineReadable := isMachineReadable(output)
|
||||
@@ -183,7 +201,7 @@ func printPulls(pulls []*gitea.PullRequest, output string, fields []string) erro
|
||||
}
|
||||
}
|
||||
// store items with printable interface
|
||||
printables[i] = &printablePull{x, &labelMap}
|
||||
printables[i] = &printablePull{x, &labelMap, &ciStatuses}
|
||||
}
|
||||
|
||||
t := tableFromItems(fields, printables, machineReadable)
|
||||
@@ -193,6 +211,7 @@ func printPulls(pulls []*gitea.PullRequest, output string, fields []string) erro
|
||||
type printablePull struct {
|
||||
*gitea.PullRequest
|
||||
formattedLabels *map[int64]string
|
||||
ciStatuses *map[int64]*gitea.CombinedStatus
|
||||
}
|
||||
|
||||
func (x printablePull) FormatField(field string, machineReadable bool) string {
|
||||
@@ -252,6 +271,13 @@ func (x printablePull) FormatField(field string, machineReadable bool) string {
|
||||
return x.DiffURL
|
||||
case "patch":
|
||||
return x.PatchURL
|
||||
case "ci":
|
||||
if x.ciStatuses != nil {
|
||||
if ci, ok := (*x.ciStatuses)[x.Index]; ok {
|
||||
return formatCIStatus(ci, machineReadable)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
73
modules/print/pull_review_comment.go
Normal file
73
modules/print/pull_review_comment.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
)
|
||||
|
||||
// PullReviewCommentFields are all available fields to print with PullReviewCommentsList()
|
||||
var PullReviewCommentFields = []string{
|
||||
"id",
|
||||
"body",
|
||||
"reviewer",
|
||||
"path",
|
||||
"line",
|
||||
"resolver",
|
||||
"created",
|
||||
"updated",
|
||||
"url",
|
||||
}
|
||||
|
||||
// PullReviewCommentsList prints a listing of pull review comments
|
||||
func PullReviewCommentsList(comments []*gitea.PullReviewComment, output string, fields []string) error {
|
||||
printables := make([]printable, len(comments))
|
||||
for i, c := range comments {
|
||||
printables[i] = &printablePullReviewComment{c}
|
||||
}
|
||||
t := tableFromItems(fields, printables, isMachineReadable(output))
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
type printablePullReviewComment struct {
|
||||
*gitea.PullReviewComment
|
||||
}
|
||||
|
||||
func (x printablePullReviewComment) FormatField(field string, machineReadable bool) string {
|
||||
switch field {
|
||||
case "id":
|
||||
return fmt.Sprintf("%d", x.ID)
|
||||
case "body":
|
||||
return x.Body
|
||||
case "reviewer":
|
||||
if x.Reviewer != nil {
|
||||
return formatUserName(x.Reviewer)
|
||||
}
|
||||
return ""
|
||||
case "path":
|
||||
return x.Path
|
||||
case "line":
|
||||
if x.LineNum != 0 {
|
||||
return fmt.Sprintf("%d", x.LineNum)
|
||||
}
|
||||
if x.OldLineNum != 0 {
|
||||
return fmt.Sprintf("%d", x.OldLineNum)
|
||||
}
|
||||
return ""
|
||||
case "resolver":
|
||||
if x.Resolver != nil {
|
||||
return formatUserName(x.Resolver)
|
||||
}
|
||||
return ""
|
||||
case "created":
|
||||
return FormatTime(x.Created, machineReadable)
|
||||
case "updated":
|
||||
return FormatTime(x.Updated, machineReadable)
|
||||
case "url":
|
||||
return x.HTMLURL
|
||||
}
|
||||
return ""
|
||||
}
|
||||
189
modules/print/pull_test.go
Normal file
189
modules/print/pull_test.go
Normal file
@@ -0,0 +1,189 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestPR(index int64, title string) *gitea.PullRequest {
|
||||
now := time.Now()
|
||||
return &gitea.PullRequest{
|
||||
Index: index,
|
||||
Title: title,
|
||||
State: gitea.StateOpen,
|
||||
Poster: &gitea.User{UserName: "testuser"},
|
||||
Head: &gitea.PRBranchInfo{Ref: "branch", Name: "branch"},
|
||||
Base: &gitea.PRBranchInfo{Ref: "main", Name: "main"},
|
||||
Created: &now,
|
||||
Updated: &now,
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatCIStatusNil(t *testing.T) {
|
||||
assert.Equal(t, "", formatCIStatus(nil, false))
|
||||
assert.Equal(t, "", formatCIStatus(nil, true))
|
||||
}
|
||||
|
||||
func TestFormatCIStatusEmpty(t *testing.T) {
|
||||
ci := &gitea.CombinedStatus{Statuses: []*gitea.Status{}}
|
||||
assert.Equal(t, "", formatCIStatus(ci, false))
|
||||
assert.Equal(t, "", formatCIStatus(ci, true))
|
||||
}
|
||||
|
||||
func TestFormatCIStatusMachineReadable(t *testing.T) {
|
||||
ci := &gitea.CombinedStatus{
|
||||
State: gitea.StatusSuccess,
|
||||
Statuses: []*gitea.Status{
|
||||
{State: gitea.StatusSuccess, Context: "lint"},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, "success", formatCIStatus(ci, true))
|
||||
|
||||
ci.State = gitea.StatusPending
|
||||
ci.Statuses = []*gitea.Status{
|
||||
{State: gitea.StatusPending, Context: "build"},
|
||||
}
|
||||
assert.Equal(t, "pending", formatCIStatus(ci, true))
|
||||
}
|
||||
|
||||
func TestFormatCIStatusSingle(t *testing.T) {
|
||||
ci := &gitea.CombinedStatus{
|
||||
State: gitea.StatusSuccess,
|
||||
Statuses: []*gitea.Status{
|
||||
{State: gitea.StatusSuccess, Context: "lint"},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, "✓ lint", formatCIStatus(ci, false))
|
||||
}
|
||||
|
||||
func TestFormatCIStatusMultiple(t *testing.T) {
|
||||
ci := &gitea.CombinedStatus{
|
||||
State: gitea.StatusFailure,
|
||||
Statuses: []*gitea.Status{
|
||||
{State: gitea.StatusSuccess, Context: "lint"},
|
||||
{State: gitea.StatusPending, Context: "build"},
|
||||
{State: gitea.StatusFailure, Context: "test"},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, "✓ lint, ⏳ build, ❌ test", formatCIStatus(ci, false))
|
||||
}
|
||||
|
||||
func TestFormatCIStatusAllStates(t *testing.T) {
|
||||
tests := []struct {
|
||||
state gitea.StatusState
|
||||
context string
|
||||
expected string
|
||||
}{
|
||||
{gitea.StatusSuccess, "s", "✓ s"},
|
||||
{gitea.StatusPending, "p", "⏳ p"},
|
||||
{gitea.StatusWarning, "w", "⚠ w"},
|
||||
{gitea.StatusError, "e", "✘ e"},
|
||||
{gitea.StatusFailure, "f", "❌ f"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
ci := &gitea.CombinedStatus{
|
||||
State: tt.state,
|
||||
Statuses: []*gitea.Status{{State: tt.state, Context: tt.context}},
|
||||
}
|
||||
assert.Equal(t, tt.expected, formatCIStatus(ci, false), "state: %s", tt.state)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullsListWithCIField(t *testing.T) {
|
||||
prs := []*gitea.PullRequest{
|
||||
newTestPR(1, "feat: add feature"),
|
||||
newTestPR(2, "fix: bug fix"),
|
||||
}
|
||||
|
||||
ciStatuses := map[int64]*gitea.CombinedStatus{
|
||||
1: {
|
||||
State: gitea.StatusSuccess,
|
||||
Statuses: []*gitea.Status{
|
||||
{State: gitea.StatusSuccess, Context: "ci/build"},
|
||||
},
|
||||
},
|
||||
2: {
|
||||
State: gitea.StatusFailure,
|
||||
Statuses: []*gitea.Status{
|
||||
{State: gitea.StatusFailure, Context: "ci/test"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
tbl := tableFromItems(
|
||||
[]string{"index", "ci"},
|
||||
[]printable{
|
||||
&printablePull{prs[0], &map[int64]string{}, &ciStatuses},
|
||||
&printablePull{prs[1], &map[int64]string{}, &ciStatuses},
|
||||
},
|
||||
true,
|
||||
)
|
||||
require.NoError(t, tbl.fprint(buf, "json"))
|
||||
|
||||
var result []map[string]string
|
||||
require.NoError(t, json.Unmarshal(buf.Bytes(), &result))
|
||||
require.Len(t, result, 2)
|
||||
assert.Equal(t, "1", result[0]["index"])
|
||||
assert.Equal(t, "success", result[0]["ci"])
|
||||
assert.Equal(t, "2", result[1]["index"])
|
||||
assert.Equal(t, "failure", result[1]["ci"])
|
||||
}
|
||||
|
||||
func TestPullsListCIFieldEmpty(t *testing.T) {
|
||||
prs := []*gitea.PullRequest{newTestPR(1, "no ci")}
|
||||
ciStatuses := map[int64]*gitea.CombinedStatus{}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
tbl := tableFromItems(
|
||||
[]string{"index", "ci"},
|
||||
[]printable{
|
||||
&printablePull{prs[0], &map[int64]string{}, &ciStatuses},
|
||||
},
|
||||
true,
|
||||
)
|
||||
require.NoError(t, tbl.fprint(buf, "json"))
|
||||
|
||||
var result []map[string]string
|
||||
require.NoError(t, json.Unmarshal(buf.Bytes(), &result))
|
||||
require.Len(t, result, 1)
|
||||
assert.Equal(t, "", result[0]["ci"])
|
||||
}
|
||||
|
||||
func TestPullsListNilCIStatusesWithCIField(t *testing.T) {
|
||||
prs := []*gitea.PullRequest{newTestPR(1, "nil ci")}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
tbl := tableFromItems(
|
||||
[]string{"index", "ci"},
|
||||
[]printable{
|
||||
&printablePull{prs[0], &map[int64]string{}, nil},
|
||||
},
|
||||
true,
|
||||
)
|
||||
require.NoError(t, tbl.fprint(buf, "json"))
|
||||
|
||||
var result []map[string]string
|
||||
require.NoError(t, json.Unmarshal(buf.Bytes(), &result))
|
||||
require.Len(t, result, 1)
|
||||
assert.Equal(t, "", result[0]["ci"])
|
||||
}
|
||||
|
||||
func TestPullsListNoCIFieldNoPanic(t *testing.T) {
|
||||
prs := []*gitea.PullRequest{newTestPR(1, "test")}
|
||||
require.NoError(t, PullsList(prs, "", []string{"index", "title"}, nil))
|
||||
}
|
||||
|
||||
func TestPullFieldsContainsCI(t *testing.T) {
|
||||
assert.True(t, slices.Contains(PullFields, "ci"), "PullFields should contain 'ci'")
|
||||
}
|
||||
@@ -150,8 +150,7 @@ func outputYaml(f io.Writer, headers []string, values [][]string) error {
|
||||
})
|
||||
|
||||
valueNode := &yaml.Node{Kind: yaml.ScalarNode, Value: val}
|
||||
intVal, _ := strconv.Atoi(val)
|
||||
if strconv.Itoa(intVal) == val {
|
||||
if _, err := strconv.ParseInt(val, 10, 64); err == nil {
|
||||
valueNode.Tag = "!!int"
|
||||
} else {
|
||||
valueNode.Tag = "!!str"
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
func CreateIssue(login *config.Login, repoOwner, repoName string, opts gitea.CreateIssueOption) error {
|
||||
// title is required
|
||||
if len(opts.Title) == 0 {
|
||||
return fmt.Errorf("Title is required")
|
||||
return fmt.Errorf("title is required")
|
||||
}
|
||||
|
||||
issue, _, err := login.Client().CreateIssue(repoOwner, repoName, opts)
|
||||
|
||||
@@ -13,8 +13,10 @@ import (
|
||||
// ResolveLabelNames returns a list of label IDs for a given list of label names
|
||||
func ResolveLabelNames(client *gitea.Client, owner, repo string, labelNames []string) ([]int64, error) {
|
||||
labelIDs := make([]int64, 0, len(labelNames))
|
||||
labels, _, err := client.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: -1},
|
||||
page := 1
|
||||
for {
|
||||
labels, resp, err := client.ListRepoLabels(owner, repo, gitea.ListLabelsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -24,6 +26,11 @@ func ResolveLabelNames(client *gitea.Client, owner, repo string, labelNames []st
|
||||
labelIDs = append(labelIDs, l.ID)
|
||||
}
|
||||
}
|
||||
if resp == nil || resp.NextPage == 0 {
|
||||
break
|
||||
}
|
||||
page = resp.NextPage
|
||||
}
|
||||
return labelIDs, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -20,12 +20,13 @@ import (
|
||||
func SetupHelper(login config.Login) (ok bool, err error) {
|
||||
// Check that the URL is not blank
|
||||
if login.URL == "" {
|
||||
return false, fmt.Errorf("Invalid gitea url")
|
||||
return false, fmt.Errorf("invalid Gitea URL")
|
||||
}
|
||||
|
||||
// get all helper to URL in git config
|
||||
helperKey := fmt.Sprintf("credential.%s.helper", login.URL)
|
||||
var currentHelpers []byte
|
||||
if currentHelpers, err = exec.Command("git", "config", "--global", "--get-all", fmt.Sprintf("credential.%s.helper", login.URL)).Output(); err != nil {
|
||||
if currentHelpers, err = exec.Command("git", "config", "--global", "--get-all", helperKey).Output(); err != nil {
|
||||
currentHelpers = []byte{}
|
||||
}
|
||||
|
||||
@@ -37,10 +38,10 @@ func SetupHelper(login config.Login) (ok bool, err error) {
|
||||
}
|
||||
|
||||
// Add tea helper
|
||||
if _, err = exec.Command("git", "config", "--global", fmt.Sprintf("credential.%s.helper", login.URL), "").Output(); err != nil {
|
||||
return false, fmt.Errorf("git config --global %s, error: %s", fmt.Sprintf("credential.%s.helper", login.URL), err)
|
||||
} else if _, err = exec.Command("git", "config", "--global", "--add", fmt.Sprintf("credential.%s.helper", login.URL), "!tea login helper").Output(); err != nil {
|
||||
return false, fmt.Errorf("git config --global --add %s %s, error: %s", fmt.Sprintf("credential.%s.helper", login.URL), "!tea login helper", err)
|
||||
if _, err = exec.Command("git", "config", "--global", helperKey, "").Output(); err != nil {
|
||||
return false, fmt.Errorf("git config --global %s, error: %s", helperKey, err)
|
||||
} else if _, err = exec.Command("git", "config", "--global", "--add", helperKey, "!tea login helper").Output(); err != nil {
|
||||
return false, fmt.Errorf("git config --global --add %s %s, error: %s", helperKey, "!tea login helper", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
@@ -62,7 +63,11 @@ func CreateLogin(name, token, user, passwd, otp, scopes, sshKey, giteaURL, sshCe
|
||||
}
|
||||
// ... if we already use this token
|
||||
if shouldCheckTokenUniqueness(token, sshAgent, sshKey, sshCertPrincipal, sshKeyFingerprint) {
|
||||
if login := config.GetLoginByToken(token); login != nil {
|
||||
login, err := config.GetLoginByToken(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if login != nil {
|
||||
return fmt.Errorf("token already been used, delete login '%s' first", login.Name)
|
||||
}
|
||||
}
|
||||
@@ -161,12 +166,20 @@ func generateToken(login config.Login, user, pass, otp, scopes string) (string,
|
||||
}
|
||||
client := login.Client(opts...)
|
||||
|
||||
tl, _, err := client.ListAccessTokens(gitea.ListAccessTokensOptions{
|
||||
ListOptions: gitea.ListOptions{Page: -1},
|
||||
var tl []*gitea.AccessToken
|
||||
for page := 1; ; {
|
||||
page_tokens, resp, err := client.ListAccessTokens(gitea.ListAccessTokensOptions{
|
||||
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tl = append(tl, page_tokens...)
|
||||
if resp == nil || resp.NextPage == 0 {
|
||||
break
|
||||
}
|
||||
page = resp.NextPage
|
||||
}
|
||||
host, _ := os.Hostname()
|
||||
tokenName := host + "-tea"
|
||||
|
||||
|
||||
@@ -19,12 +19,23 @@ import (
|
||||
// a matching private key in ~/.ssh/. If no match is found, path is empty.
|
||||
func findSSHKey(client *gitea.Client) (string, error) {
|
||||
// get keys registered on gitea instance
|
||||
keys, _, err := client.ListMyPublicKeys(gitea.ListPublicKeysOptions{
|
||||
ListOptions: gitea.ListOptions{Page: -1},
|
||||
var keys []*gitea.PublicKey
|
||||
for page := 1; ; {
|
||||
page_keys, resp, err := client.ListMyPublicKeys(gitea.ListPublicKeysOptions{
|
||||
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||
})
|
||||
if err != nil || len(keys) == 0 {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
keys = append(keys, page_keys...)
|
||||
if resp == nil || resp.NextPage == 0 {
|
||||
break
|
||||
}
|
||||
page = resp.NextPage
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// enumerate ~/.ssh/*.pub files
|
||||
glob, err := utils.AbsPathWithExpansion("~/.ssh/*.pub")
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
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")
|
||||
return fmt.Errorf("title is required")
|
||||
}
|
||||
|
||||
mile, _, err := login.Client().CreateMilestone(repoOwner, repoName, gitea.CreateMilestoneOption{
|
||||
|
||||
@@ -39,7 +39,7 @@ func PullClean(login *config.Login, repoOwner, repoName string, index int64, ign
|
||||
|
||||
// if remote head branch is already deleted, pr.Head.Ref points to "pulls/<idx>/head"
|
||||
remoteBranch := pr.Head.Ref
|
||||
remoteDeleted := remoteBranch == fmt.Sprintf("refs/pull/%d/head", pr.Index)
|
||||
remoteDeleted := isRemoteDeleted(pr)
|
||||
if remoteDeleted {
|
||||
remoteBranch = pr.Head.Name // this still holds the original branch name
|
||||
fmt.Printf("Remote branch '%s' already deleted.\n", remoteBranch)
|
||||
@@ -62,9 +62,9 @@ func PullClean(login *config.Login, repoOwner, repoName string, index int64, ign
|
||||
}
|
||||
if branch == nil {
|
||||
if ignoreSHA {
|
||||
return fmt.Errorf("Remote branch %s not found in local repo", remoteBranch)
|
||||
return fmt.Errorf("remote branch %s not found in local repo", remoteBranch)
|
||||
}
|
||||
return fmt.Errorf(`Remote branch %s not found in local repo.
|
||||
return fmt.Errorf(`remote branch %s not found in local repo.
|
||||
Either you don't track this PR, or the local branch has diverged from the remote.
|
||||
If you still want to continue & are sure you don't loose any important commits,
|
||||
call me again with the --ignore-sha flag`, remoteBranch)
|
||||
|
||||
@@ -18,7 +18,7 @@ func PullMerge(login *config.Login, repoOwner, repoName string, index int64, opt
|
||||
return err
|
||||
}
|
||||
if !success {
|
||||
return fmt.Errorf("Failed to merge PR. Is it still open?")
|
||||
return fmt.Errorf("failed to merge PR, is it still open?")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
68
modules/task/pull_review_comment.go
Normal file
68
modules/task/pull_review_comment.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"code.gitea.io/tea/modules/context"
|
||||
)
|
||||
|
||||
// ListPullReviewComments lists all review comments across all reviews for a PR
|
||||
func ListPullReviewComments(ctx *context.TeaContext, idx int64) ([]*gitea.PullReviewComment, error) {
|
||||
c := ctx.Login.Client()
|
||||
|
||||
var reviews []*gitea.PullReview
|
||||
for page := 1; ; {
|
||||
page_reviews, resp, err := c.ListPullReviews(ctx.Owner, ctx.Repo, idx, gitea.ListPullReviewsOptions{
|
||||
ListOptions: gitea.ListOptions{Page: page, PageSize: 50},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reviews = append(reviews, page_reviews...)
|
||||
if resp == nil || resp.NextPage == 0 {
|
||||
break
|
||||
}
|
||||
page = resp.NextPage
|
||||
}
|
||||
|
||||
var allComments []*gitea.PullReviewComment
|
||||
for _, review := range reviews {
|
||||
comments, _, err := c.ListPullReviewComments(ctx.Owner, ctx.Repo, idx, review.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allComments = append(allComments, comments...)
|
||||
}
|
||||
|
||||
return allComments, nil
|
||||
}
|
||||
|
||||
// ResolvePullReviewComment resolves a review comment
|
||||
func ResolvePullReviewComment(ctx *context.TeaContext, commentID int64) error {
|
||||
c := ctx.Login.Client()
|
||||
|
||||
_, err := c.ResolvePullReviewComment(ctx.Owner, ctx.Repo, commentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Comment %d resolved\n", commentID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnresolvePullReviewComment unresolves a review comment
|
||||
func UnresolvePullReviewComment(ctx *context.TeaContext, commentID int64) error {
|
||||
c := ctx.Login.Client()
|
||||
|
||||
_, err := c.UnresolvePullReviewComment(ctx.Owner, ctx.Repo, commentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Comment %d unresolved\n", commentID)
|
||||
return nil
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// PathExists returns whether the given file or directory exists or not
|
||||
@@ -38,18 +39,19 @@ func exists(path string, expectDir bool) (bool, error) {
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return false, nil
|
||||
} else if err.(*os.PathError).Err.Error() == "not a directory" {
|
||||
// some middle segment of path is a file, cannot traverse
|
||||
// FIXME: catches error on linux; go does not provide a way to catch this properly..
|
||||
}
|
||||
var pathErr *os.PathError
|
||||
if errors.As(err, &pathErr) && errors.Is(pathErr.Err, syscall.ENOTDIR) {
|
||||
// a middle segment of path is a file, cannot traverse
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
isDir := f.IsDir()
|
||||
if isDir && !expectDir {
|
||||
return false, errors.New("A directory with the same name exists")
|
||||
return false, errors.New("a directory with the same name exists")
|
||||
} else if !isDir && expectDir {
|
||||
return false, errors.New("A file with the same name exists")
|
||||
return false, errors.New("a file with the same name exists")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -21,17 +21,17 @@ func ValidateAuthenticationMethod(
|
||||
// Normalize URL
|
||||
serverURL, err := NormalizeURL(giteaURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Unable to parse URL: %s", err)
|
||||
return nil, fmt.Errorf("unable to parse URL: %s", err)
|
||||
}
|
||||
|
||||
if !sshAgent && sshCertPrincipal == "" && sshKey == "" {
|
||||
// .. if we have enough information to authenticate
|
||||
if len(token) == 0 && (len(user)+len(passwd)) == 0 {
|
||||
return nil, fmt.Errorf("No token set")
|
||||
return nil, fmt.Errorf("no token set")
|
||||
} else if len(user) != 0 && len(passwd) == 0 {
|
||||
return nil, fmt.Errorf("No password set")
|
||||
return nil, fmt.Errorf("no password set")
|
||||
} else if len(user) == 0 && len(passwd) != 0 {
|
||||
return nil, fmt.Errorf("No user set")
|
||||
return nil, fmt.Errorf("no user set")
|
||||
}
|
||||
}
|
||||
return serverURL, nil
|
||||
|
||||
Reference in New Issue
Block a user