15 Commits

Author SHA1 Message Date
Hugo Nijhuis-Mekkelholt
6e88132305 chore: retrigger CI
Some checks failed
check-and-test / Run govulncheck (pull_request) Successful in 36s
check-and-test / check-and-test (pull_request) Failing after 1m21s
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 20:07:01 +01:00
Hugo Nijhuis-Mekkelholt
38f93a4997 docs: generate documentation for actions commands
Some checks failed
check-and-test / Run govulncheck (pull_request) Successful in 2m18s
check-and-test / check-and-test (pull_request) Failing after 2m22s
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 20:02:57 +01:00
Hugo Nijhuis-Mekkelholt
9fdbd4aafc fix: formatting and error message capitalization
Some checks failed
check-and-test / check-and-test (pull_request) Failing after 1m34s
check-and-test / Run govulncheck (pull_request) Successful in 1m54s
- Run go fmt on modules/api/types.go to fix struct alignment
- Use lowercase error messages to match codebase conventions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 19:57:49 +01:00
Hugo Nijhuis-Mekkelholt
7aa00e6f54 fix: address code review feedback
Some checks failed
check-and-test / check-and-test (pull_request) Failing after 1m38s
check-and-test / Run govulncheck (pull_request) Successful in 1m55s
- Fix GetRaw() error handling to check status before reading body
- Add context.Context support to all HTTP requests
- Use consistent capitalized error messages
- Update copyright year from 2024 to 2025
- Add unit tests for modules/api/client.go
- Add tests for runs, jobs, logs commands

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 19:51:52 +01:00
Hugo Nijhuis-Mekkelholt
cea501523e feat(actions): add runs, jobs, and logs commands
Some checks failed
check-and-test / Run govulncheck (pull_request) Successful in 1m43s
check-and-test / check-and-test (pull_request) Failing after 2m29s
Implement tea actions commands to view workflow runs and logs using
the Gitea 1.25 API endpoints directly. This adds:
- `tea actions runs` - list workflow runs for a repository
- `tea actions jobs <run-id>` - list jobs for a specific run
- `tea actions logs <job-id>` - display logs for a specific job

Also adds a new `modules/api` package for making raw authenticated
HTTP requests to Gitea API endpoints not yet supported by the go-sdk.

Closes #1

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 19:40:19 +01:00
Alain Thiffault
68b9620b8c fix: expose pagination flags for secrets list command (#853)
Some checks failed
goreleaser / goreleaser (push) Failing after 1m29s
goreleaser / release-image (push) Failing after 2m54s
The command uses flags.GetListOptions() internally but didn't expose --page and --limit flags to users, making pagination inaccessible.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/853
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Alain Thiffault <athiffau@effectivemomentum.com>
Co-committed-by: Alain Thiffault <athiffau@effectivemomentum.com>
2025-12-05 06:14:41 +00:00
Alain Thiffault
e961a8f01d fix: expose pagination flags for webhooks list command (#852)
The command uses flags.GetListOptions() internally but didn't expose --page and --limit flags to users, making pagination inaccessible.

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/852
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Alain Thiffault <athiffau@effectivemomentum.com>
Co-committed-by: Alain Thiffault <athiffau@effectivemomentum.com>
2025-12-05 06:14:34 +00:00
Alain Thiffault
f59430a42a fix: pass pagination options to ListRepoPullRequests (#851)
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/851
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Alain Thiffault <athiffau@effectivemomentum.com>
Co-committed-by: Alain Thiffault <athiffau@effectivemomentum.com>
2025-12-05 06:14:01 +00:00
Lunny Xiao
7e2e7ee809 Fix delete repo description (#858)
Fix #857

Reviewed-on: https://gitea.com/gitea/tea/pulls/858
2025-12-05 06:11:38 +00:00
Riccardo Förster
1d1d9197ee feat(issue): Add JSON output and file redirection (#841)
This change enhances the 'issue' command functionality by enabling structured JSON
output for single issue views and introducing a method for output redirection.

**Changes Implemented:**

1. Enables the existing `--output json` flag for single issue commands (e.g., 'tea issue 17'). This flag was previously ignored in this context.
2. Introduces the new `--out <filename>` flag, which redirects the marshaled JSON output from stdout to the specified file.

Feeback more then welcome.

Co-authored-by: Jonas Toth <development@jonas-toth.eu>
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/841
Reviewed-by: TheFox0x7 <thefox0x7@noreply.gitea.com>
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Riccardo Förster <riccardo.foerster@sarad.de>
Co-committed-by: Riccardo Förster <riccardo.foerster@sarad.de>
2025-11-29 05:05:30 +00:00
TheFox0x7
f6d4b5fa4f remove group readwrite permission (#856)
closes: https://gitea.com/gitea/tea/issues/855
Reviewed-on: https://gitea.com/gitea/tea/pulls/856
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: TheFox0x7 <thefox0x7@gmail.com>
Co-committed-by: TheFox0x7 <thefox0x7@gmail.com>
2025-11-27 22:45:25 +00:00
Brandon Martin
016e068c60 Fix: Enable git worktree support and improve pr create error handling (#850)
## Problem

Tea commands fail when run from git worktrees with the error:
Remote repository required: Specify ID via --repo or execute from a
local git repo.

Even though the worktree is in a valid git repository with remotes
configured.

Additionally, `tea pr create` was missing context validation, showing
cryptic errors like `"path segment [0]
is empty"` instead of helpful messages.

## Root Cause

1. **Worktree issue**: go-git's `PlainOpenWithOptions` was not
configured to read the `commondir` file that
git worktrees use. This file points to the main repository's `.git`
directory where remotes are actually
stored (worktrees don't have their own remotes).

2. **PR create issue**: Missing `ctx.Ensure()` validation meant errors
weren't caught early with clear
messages.

## Solution

### 1. Enable worktree support (`modules/git/repo.go`)
```go
EnableDotGitCommonDir: true, // Enable commondir support for worktrees

This tells go-git to:
- Read the commondir file in .git/worktrees/<name>/commondir
- Follow the reference (typically ../..) to the main repository
- Load remotes from the main repo's config

2. Add context validation (cmd/pulls/create.go)

ctx.Ensure(context.CtxRequirement{
LocalRepo:  true,
RemoteRepo: true,
})

Provides clear error messages and matches the pattern used in pr
checkout (fixed in commit 0970b945 from
2020).

3. Add test coverage (modules/git/repo_test.go)

- Creates a real git repository with a worktree
- Verifies that RepoFromPath() can open the worktree
- Confirms that Config() correctly reads remotes from main repo

Test Results

Without fix:
 FAIL: Should NOT be empty, but was map[]

With fix:
 PASS: TestRepoFromPath_Worktree (0.12s)

Manual test in worktree:
cd /path/to/worktree
tea pr create --title "test"
# Now works! 

Checklist

- Tested manually in a git worktree
- Added test case that fails without the fix
- All existing tests pass

Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
Reviewed-on: https://gitea.com/gitea/tea/pulls/850
Reviewed-by: techknowlogick <techknowlogick@noreply.gitea.com>
Co-authored-by: Brandon Martin <brandon@codedmart.com>
Co-committed-by: Brandon Martin <brandon@codedmart.com>
2025-11-24 22:21:19 +00:00
Lunny Xiao
587b31503d Upgrade dependencies (#849)
Reviewed-on: https://gitea.com/gitea/tea/pulls/849
2025-11-24 19:21:55 +00:00
qwerty287
4877f181fb Only prompt for SSH passphrase if necessary (#844)
Since one of the last updates (I cannot tell you exactly which one, but likely 0.10 or 0.11), tea always asks me for my ssh passphrase without actually needing it. I do not have anything configured regarding SSH keys.

The passphrase is not even verified, you can enter anything there. But as this is quite annoying, I fixed this by moving the prompt to only be used when a ssh key/cert is configured.

Would be nice to get this in. Thanks!

Reviewed-on: https://gitea.com/gitea/tea/pulls/844
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: qwerty287 <qwerty287@posteo.de>
Co-committed-by: qwerty287 <qwerty287@posteo.de>
2025-11-20 01:32:28 +00:00
Ross Golder
81481f8f9d Fix: Only prompt for login confirmation when no default login is set (#839)
When running tea commands outside of a repository context, tea falls back to using the default login but always prompted for confirmation, even when a default was set. This fix only prompts when no default is configured.

Reviewed-on: https://gitea.com/gitea/tea/pulls/839
Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Ross Golder <ross@golder.org>
Co-committed-by: Ross Golder <ross@golder.org>
2025-10-27 17:52:04 +00:00
29 changed files with 1442 additions and 129 deletions

View File

@@ -22,6 +22,9 @@ var CmdActions = cli.Command{
Commands: []*cli.Command{
&actions.CmdActionsSecrets,
&actions.CmdActionsVariables,
&actions.CmdActionsRuns,
&actions.CmdActionsJobs,
&actions.CmdActionsLogs,
},
Flags: []cli.Flag{
&cli.StringFlag{

56
cmd/actions/jobs.go Normal file
View File

@@ -0,0 +1,56 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
stdctx "context"
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/api"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v3"
)
// CmdActionsJobs represents the actions jobs command
var CmdActionsJobs = cli.Command{
Name: "jobs",
Aliases: []string{"job"},
Usage: "List jobs for a workflow run",
Description: "List jobs for a specific workflow run",
ArgsUsage: "<run-id>",
Action: RunActionJobs,
Flags: flags.AllDefaultFlags,
}
// RunActionJobs lists jobs for a workflow run
func RunActionJobs(ctx stdctx.Context, cmd *cli.Command) error {
c := context.InitCommand(cmd)
c.Ensure(context.CtxRequirement{RemoteRepo: true})
if cmd.Args().Len() < 1 {
return fmt.Errorf("run ID is required")
}
runID, err := utils.ArgToIndex(cmd.Args().First())
if err != nil {
return fmt.Errorf("invalid run ID: %w", err)
}
client := api.NewClient(c.Login)
path := fmt.Sprintf("/repos/%s/%s/actions/runs/%d/jobs",
c.Owner, c.Repo, runID)
var jobs api.ActionJobList
if _, err := client.Get(ctx, path, &jobs); err != nil {
return err
}
print.ActionJobsList(jobs.Jobs, c.Output)
return nil
}

62
cmd/actions/jobs_test.go Normal file
View File

@@ -0,0 +1,62 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
)
func TestJobsCommandFlags(t *testing.T) {
cmd := CmdActionsJobs
// Test that required flags exist
expectedFlags := []string{"output", "remote", "login", "repo"}
for _, flagName := range expectedFlags {
found := false
for _, flag := range cmd.Flags {
for _, name := range flag.Names() {
if name == flagName {
found = true
break
}
}
if found {
break
}
}
if !found {
t.Errorf("Expected flag %s not found in CmdActionsJobs", flagName)
}
}
}
func TestJobsCommandProperties(t *testing.T) {
cmd := CmdActionsJobs
if cmd.Name != "jobs" {
t.Errorf("Expected command name 'jobs', got %s", cmd.Name)
}
if len(cmd.Aliases) == 0 || cmd.Aliases[0] != "job" {
t.Errorf("Expected alias 'job' for jobs command")
}
if cmd.Usage == "" {
t.Error("Jobs command should have usage text")
}
if cmd.Description == "" {
t.Error("Jobs command should have description")
}
if cmd.ArgsUsage != "<run-id>" {
t.Errorf("Expected ArgsUsage '<run-id>', got %s", cmd.ArgsUsage)
}
if cmd.Action == nil {
t.Error("Jobs command should have an action")
}
}

55
cmd/actions/logs.go Normal file
View File

@@ -0,0 +1,55 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
stdctx "context"
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/api"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/utils"
"github.com/urfave/cli/v3"
)
// CmdActionsLogs represents the actions logs command
var CmdActionsLogs = cli.Command{
Name: "logs",
Aliases: []string{"log"},
Usage: "Display logs for a job",
Description: "Display logs for a specific job",
ArgsUsage: "<job-id>",
Action: RunActionLogs,
Flags: flags.AllDefaultFlags,
}
// RunActionLogs displays logs for a job
func RunActionLogs(ctx stdctx.Context, cmd *cli.Command) error {
c := context.InitCommand(cmd)
c.Ensure(context.CtxRequirement{RemoteRepo: true})
if cmd.Args().Len() < 1 {
return fmt.Errorf("job ID is required")
}
jobID, err := utils.ArgToIndex(cmd.Args().First())
if err != nil {
return fmt.Errorf("invalid job ID: %w", err)
}
client := api.NewClient(c.Login)
path := fmt.Sprintf("/repos/%s/%s/actions/jobs/%d/logs",
c.Owner, c.Repo, jobID)
logs, err := client.GetRaw(ctx, path)
if err != nil {
return err
}
fmt.Print(string(logs))
return nil
}

62
cmd/actions/logs_test.go Normal file
View File

@@ -0,0 +1,62 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
)
func TestLogsCommandFlags(t *testing.T) {
cmd := CmdActionsLogs
// Test that required flags exist
expectedFlags := []string{"output", "remote", "login", "repo"}
for _, flagName := range expectedFlags {
found := false
for _, flag := range cmd.Flags {
for _, name := range flag.Names() {
if name == flagName {
found = true
break
}
}
if found {
break
}
}
if !found {
t.Errorf("Expected flag %s not found in CmdActionsLogs", flagName)
}
}
}
func TestLogsCommandProperties(t *testing.T) {
cmd := CmdActionsLogs
if cmd.Name != "logs" {
t.Errorf("Expected command name 'logs', got %s", cmd.Name)
}
if len(cmd.Aliases) == 0 || cmd.Aliases[0] != "log" {
t.Errorf("Expected alias 'log' for logs command")
}
if cmd.Usage == "" {
t.Error("Logs command should have usage text")
}
if cmd.Description == "" {
t.Error("Logs command should have description")
}
if cmd.ArgsUsage != "<job-id>" {
t.Errorf("Expected ArgsUsage '<job-id>', got %s", cmd.ArgsUsage)
}
if cmd.Action == nil {
t.Error("Logs command should have an action")
}
}

50
cmd/actions/runs.go Normal file
View File

@@ -0,0 +1,50 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
stdctx "context"
"fmt"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/modules/api"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/print"
"github.com/urfave/cli/v3"
)
// CmdActionsRuns represents the actions runs command
var CmdActionsRuns = cli.Command{
Name: "runs",
Aliases: []string{"run"},
Usage: "List workflow runs",
Description: "List workflow runs for a repository",
Action: RunActionRuns,
Flags: append([]cli.Flag{
&flags.PaginationPageFlag,
&flags.PaginationLimitFlag,
}, flags.AllDefaultFlags...),
}
// RunActionRuns lists workflow runs
func RunActionRuns(ctx stdctx.Context, cmd *cli.Command) error {
c := context.InitCommand(cmd)
c.Ensure(context.CtxRequirement{RemoteRepo: true})
client := api.NewClient(c.Login)
path := fmt.Sprintf("/repos/%s/%s/actions/runs?page=%d&limit=%d",
c.Owner, c.Repo,
flags.GetListOptions().Page,
flags.GetListOptions().PageSize)
var runs api.ActionRunList
if _, err := client.Get(ctx, path, &runs); err != nil {
return err
}
print.ActionRunsList(runs.WorkflowRuns, c.Output)
return nil
}

58
cmd/actions/runs_test.go Normal file
View File

@@ -0,0 +1,58 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
)
func TestRunsCommandFlags(t *testing.T) {
cmd := CmdActionsRuns
// Test that required flags exist
expectedFlags := []string{"page", "limit", "output", "remote", "login", "repo"}
for _, flagName := range expectedFlags {
found := false
for _, flag := range cmd.Flags {
for _, name := range flag.Names() {
if name == flagName {
found = true
break
}
}
if found {
break
}
}
if !found {
t.Errorf("Expected flag %s not found in CmdActionsRuns", flagName)
}
}
}
func TestRunsCommandProperties(t *testing.T) {
cmd := CmdActionsRuns
if cmd.Name != "runs" {
t.Errorf("Expected command name 'runs', got %s", cmd.Name)
}
if len(cmd.Aliases) == 0 || cmd.Aliases[0] != "run" {
t.Errorf("Expected alias 'run' for runs command")
}
if cmd.Usage == "" {
t.Error("Runs command should have usage text")
}
if cmd.Description == "" {
t.Error("Runs command should have description")
}
if cmd.Action == nil {
t.Error("Runs command should have an action")
}
}

View File

@@ -21,7 +21,10 @@ var CmdSecretsList = cli.Command{
Usage: "List action secrets",
Description: "List secrets configured for repository actions",
Action: RunSecretsList,
Flags: flags.AllDefaultFlags,
Flags: append([]cli.Flag{
&flags.PaginationPageFlag,
&flags.PaginationLimitFlag,
}, flags.AllDefaultFlags...),
}
// RunSecretsList list action secrets

View File

@@ -5,8 +5,12 @@ package cmd
import (
stdctx "context"
"encoding/json"
"fmt"
"time"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/cmd/flags"
"code.gitea.io/tea/cmd/issues"
"code.gitea.io/tea/modules/context"
"code.gitea.io/tea/modules/interact"
@@ -16,6 +20,34 @@ import (
"github.com/urfave/cli/v3"
)
type labelData struct {
Name string `json:"name"`
Color string `json:"color"`
Description string `json:"description"`
}
type issueData struct {
ID int64 `json:"id"`
Index int64 `json:"index"`
Title string `json:"title"`
State gitea.StateType `json:"state"`
Created time.Time `json:"created"`
Labels []labelData `json:"labels"`
User string `json:"user"`
Body string `json:"body"`
Assignees []string `json:"assignees"`
URL string `json:"url"`
ClosedAt *time.Time `json:"closedAt"`
Comments []commentData `json:"comments"`
}
type commentData struct {
ID int64 `json:"id"`
Author string `json:"author"`
Created time.Time `json:"created"`
Body string `json:"body"`
}
// CmdIssues represents to login a gitea server.
var CmdIssues = cli.Command{
Name: "issues",
@@ -64,6 +96,14 @@ func runIssueDetail(_ stdctx.Context, cmd *cli.Command, index string) error {
if err != nil {
return err
}
if ctx.IsSet("output") {
switch ctx.String("output") {
case "json":
return runIssueDetailAsJSON(ctx, issue)
}
}
print.IssueDetails(issue, reactions)
if issue.Comments > 0 {
@@ -75,3 +115,61 @@ func runIssueDetail(_ stdctx.Context, cmd *cli.Command, index string) error {
return nil
}
func runIssueDetailAsJSON(ctx *context.TeaContext, issue *gitea.Issue) error {
c := ctx.Login.Client()
opts := gitea.ListIssueCommentOptions{ListOptions: flags.GetListOptions()}
labelSlice := make([]labelData, 0, len(issue.Labels))
for _, label := range issue.Labels {
labelSlice = append(labelSlice, labelData{label.Name, label.Color, label.Description})
}
assigneesSlice := make([]string, 0, len(issue.Assignees))
for _, assignee := range issue.Assignees {
assigneesSlice = append(assigneesSlice, assignee.UserName)
}
issueSlice := issueData{
ID: issue.ID,
Index: issue.Index,
Title: issue.Title,
State: issue.State,
Created: issue.Created,
User: issue.Poster.UserName,
Body: issue.Body,
Labels: labelSlice,
Assignees: assigneesSlice,
URL: issue.HTMLURL,
ClosedAt: issue.Closed,
Comments: make([]commentData, 0),
}
if ctx.Bool("comments") {
comments, _, err := c.ListIssueComments(ctx.Owner, ctx.Repo, issue.Index, opts)
issueSlice.Comments = make([]commentData, 0, len(comments))
if err != nil {
return err
}
for _, comment := range comments {
issueSlice.Comments = append(issueSlice.Comments, commentData{
ID: comment.ID,
Author: comment.Poster.UserName,
Body: comment.Body, // Selected Field
Created: comment.Created,
})
}
}
jsonData, err := json.MarshalIndent(issueSlice, "", "\t")
if err != nil {
return err
}
_, err = fmt.Fprintf(ctx.Writer, "%s\n", jsonData)
return err
}

View File

@@ -5,6 +5,7 @@ package issues
import (
stdctx "context"
"errors"
"fmt"
"code.gitea.io/tea/cmd/flags"
@@ -23,7 +24,7 @@ var CmdIssuesClose = cli.Command{
Description: `Change state of one ore more issues to 'closed'`,
ArgsUsage: "<issue index> [<issue index>...]",
Action: func(ctx stdctx.Context, cmd *cli.Command) error {
var s = gitea.StateClosed
s := gitea.StateClosed
return editIssueState(ctx, cmd, gitea.EditIssueOption{State: &s})
},
Flags: flags.AllDefaultFlags,
@@ -34,7 +35,7 @@ func editIssueState(_ stdctx.Context, cmd *cli.Command, opts gitea.EditIssueOpti
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{RemoteRepo: true})
if ctx.Args().Len() == 0 {
return fmt.Errorf(ctx.Command.ArgsUsage)
return errors.New(ctx.Command.ArgsUsage)
}
indices, err := utils.ArgsToIndices(ctx.Args().Slice())

270
cmd/issues_test.go Normal file
View File

@@ -0,0 +1,270 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package cmd
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/config"
"code.gitea.io/tea/modules/context"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
)
const (
testOwner = "testOwner"
testRepo = "testRepo"
)
func createTestIssue(comments int, isClosed bool) gitea.Issue {
var issue = gitea.Issue{
ID: 42,
Index: 1,
Title: "Test issue",
State: gitea.StateOpen,
Body: "This is a test",
Created: time.Date(2025, 31, 10, 23, 59, 59, 999999999, time.UTC),
Updated: time.Date(2025, 1, 11, 0, 0, 0, 0, time.UTC),
Labels: []*gitea.Label{
{
Name: "example/Label1",
Color: "very red",
Description: "This is an example label",
},
{
Name: "example/Label2",
Color: "hardly red",
Description: "This is another example label",
},
},
Comments: comments,
Poster: &gitea.User{
UserName: "testUser",
},
Assignees: []*gitea.User{
{UserName: "testUser"},
{UserName: "testUser3"},
},
HTMLURL: "<space holder>",
Closed: nil, //2025-11-10T21:20:19Z
}
if isClosed {
var closed = time.Date(2025, 11, 10, 21, 20, 19, 0, time.UTC)
issue.Closed = &closed
}
if isClosed {
issue.State = gitea.StateClosed
} else {
issue.State = gitea.StateOpen
}
return issue
}
func createTestIssueComments(comments int) []gitea.Comment {
baseID := 900
var result []gitea.Comment
for commentID := 0; commentID < comments; commentID++ {
result = append(result, gitea.Comment{
ID: int64(baseID + commentID),
Poster: &gitea.User{
UserName: "Freddy",
},
Body: fmt.Sprintf("This is a test comment #%v", commentID),
Created: time.Date(2025, 11, 3, 12, 0, 0, 0, time.UTC).
Add(time.Duration(commentID) * time.Hour),
})
}
return result
}
func TestRunIssueDetailAsJSON(t *testing.T) {
type TestCase struct {
name string
issue gitea.Issue
comments []gitea.Comment
flagComments bool
flagOutput string
flagOut string
closed bool
}
cmd := cli.Command{
Name: "t",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "comments",
Value: false,
},
&cli.StringFlag{
Name: "output",
Value: "json",
},
},
}
testContext := context.TeaContext{
Owner: testOwner,
Repo: testRepo,
Login: &config.Login{
Name: "testLogin",
URL: "http://127.0.0.1:8081",
},
Command: &cmd,
}
testCases := []TestCase{
{
name: "Simple issue with no comments, no comments requested",
issue: createTestIssue(0, true),
comments: []gitea.Comment{},
flagComments: false,
},
{
name: "Simple issue with no comments, comments requested",
issue: createTestIssue(0, true),
comments: []gitea.Comment{},
flagComments: true,
},
{
name: "Simple issue with comments, no comments requested",
issue: createTestIssue(2, true),
comments: createTestIssueComments(2),
flagComments: false,
},
{
name: "Simple issue with comments, comments requested",
issue: createTestIssue(2, true),
comments: createTestIssueComments(2),
flagComments: true,
},
{
name: "Simple issue with comments, comments requested, not closed",
issue: createTestIssue(2, false),
comments: createTestIssueComments(2),
flagComments: true,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if path == fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/comments", testOwner, testRepo, testCase.issue.Index) {
jsonComments, err := json.Marshal(testCase.comments)
if err != nil {
require.NoError(t, err, "Testing setup failed: failed to marshal comments")
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err = w.Write(jsonComments)
require.NoError(t, err, "Testing setup failed: failed to write out comments")
} else {
http.NotFound(w, r)
}
})
server := httptest.NewServer(handler)
testContext.Login.URL = server.URL
testCase.issue.HTMLURL = fmt.Sprintf("%s/%s/%s/issues/%d/", testContext.Login.URL, testOwner, testRepo, testCase.issue.Index)
var outBuffer bytes.Buffer
testContext.Writer = &outBuffer
var errBuffer bytes.Buffer
testContext.ErrWriter = &errBuffer
if testCase.flagComments {
_ = testContext.Command.Set("comments", "true")
} else {
_ = testContext.Command.Set("comments", "false")
}
err := runIssueDetailAsJSON(&testContext, &testCase.issue)
server.Close()
require.NoError(t, err, "Failed to run issue detail as JSON")
out := outBuffer.String()
require.NotEmpty(t, out, "Unexpected empty output from runIssueDetailAsJSON")
//setting expectations
var expectedLabels []labelData
expectedLabels = []labelData{}
for _, l := range testCase.issue.Labels {
expectedLabels = append(expectedLabels, labelData{
Name: l.Name,
Color: l.Color,
Description: l.Description,
})
}
var expectedAssignees []string
expectedAssignees = []string{}
for _, a := range testCase.issue.Assignees {
expectedAssignees = append(expectedAssignees, a.UserName)
}
var expectedClosedAt *time.Time
if testCase.issue.Closed != nil {
expectedClosedAt = testCase.issue.Closed
}
var expectedComments []commentData
expectedComments = []commentData{}
if testCase.flagComments {
for _, c := range testCase.comments {
expectedComments = append(expectedComments, commentData{
ID: c.ID,
Author: c.Poster.UserName,
Body: c.Body,
Created: c.Created,
})
}
}
expected := issueData{
ID: testCase.issue.ID,
Index: testCase.issue.Index,
Title: testCase.issue.Title,
State: testCase.issue.State,
Created: testCase.issue.Created,
User: testCase.issue.Poster.UserName,
Body: testCase.issue.Body,
URL: testCase.issue.HTMLURL,
ClosedAt: expectedClosedAt,
Labels: expectedLabels,
Assignees: expectedAssignees,
Comments: expectedComments,
}
// validating reality
var actual issueData
dec := json.NewDecoder(bytes.NewReader(outBuffer.Bytes()))
dec.DisallowUnknownFields()
err = dec.Decode(&actual)
require.NoError(t, err, "Failed to unmarshal output into struct")
assert.Equal(t, expected, actual, "Expected structs differ from expected one")
})
}
}

View File

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

View File

@@ -42,6 +42,10 @@ var CmdPullsCreate = cli.Command{
func runPullsCreate(_ stdctx.Context, cmd *cli.Command) error {
ctx := context.InitCommand(cmd)
ctx.Ensure(context.CtxRequirement{
LocalRepo: true,
RemoteRepo: true,
})
// no args -> interactive mode
if ctx.NumFlags() == 0 {

View File

@@ -44,6 +44,7 @@ func RunPullsList(_ stdctx.Context, cmd *cli.Command) error {
}
prs, _, err := ctx.Login.Client().ListRepoPullRequests(ctx.Owner, ctx.Repo, gitea.ListPullRequestsOptions{
ListOptions: flags.GetListOptions(),
State: state,
})

View File

@@ -19,7 +19,7 @@ var CmdRepoRm = cli.Command{
Name: "delete",
Aliases: []string{"rm"},
Usage: "Delete an existing repository",
Description: "Removes a repository from Create a repository from an existing repo",
Description: "Removes a repository from your Gitea instance",
ArgsUsage: " ", // command does not accept arguments
Action: runRepoDelete,
Flags: append([]cli.Flag{

View File

@@ -22,7 +22,10 @@ var CmdWebhooksList = cli.Command{
Usage: "List webhooks",
Description: "List webhooks in repository, organization, or globally",
Action: RunWebhooksList,
Flags: flags.AllDefaultFlags,
Flags: append([]cli.Flag{
&flags.PaginationPageFlag,
&flags.PaginationLimitFlag,
}, flags.AllDefaultFlags...),
}
// RunWebhooksList list webhooks

View File

@@ -67,7 +67,7 @@ Add a Gitea login
**--token, -t**="": Access token. Can be obtained from Settings > Applications
**--url, -u**="": Server URL (default: https://gitea.com)
**--url, -u**="": Server URL (default: "https://gitea.com")
**--user**="": User for basic auth (will create token)
@@ -111,7 +111,7 @@ List, create and update issues
**--fields, -f**="": Comma-separated list of fields to print. Available values:
index,state,kind,author,author-id,url,title,body,created,updated,deadline,assignees,milestone,labels,comments,owner,repo
(default: index,title,state,author,milestone,labels,owner,repo)
(default: "index,title,state,author,milestone,labels,owner,repo")
**--from, -F**="": Filter by activity after this date
@@ -157,7 +157,7 @@ List issues of the repository
**--fields, -f**="": Comma-separated list of fields to print. Available values:
index,state,kind,author,author-id,url,title,body,created,updated,deadline,assignees,milestone,labels,comments,owner,repo
(default: index,title,state,author,milestone,labels,owner,repo)
(default: "index,title,state,author,milestone,labels,owner,repo")
**--from, -F**="": Filter by activity after this date
@@ -275,7 +275,7 @@ Manage and checkout pull requests
**--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
(default: index,title,state,author,milestone,updated,labels)
(default: "index,title,state,author,milestone,updated,labels")
**--limit, --lm**="": specify limit of items per page (default: 30)
@@ -297,7 +297,7 @@ 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
(default: index,title,state,author,milestone,updated,labels)
(default: "index,title,state,author,milestone,updated,labels")
**--limit, --lm**="": specify limit of items per page (default: 30)
@@ -445,7 +445,7 @@ Merge a pull request
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
**--style, -s**="": Kind of merge to perform: merge, rebase, squash, rebase-merge (default: merge)
**--style, -s**="": Kind of merge to perform: merge, rebase, squash, rebase-merge (default: "merge")
**--title, -t**="": Merge commit title
@@ -545,7 +545,7 @@ List and create milestones
**--fields, -f**="": Comma-separated list of fields to print. Available values:
title,state,items_open,items_closed,items,duedate,description,created,updated,closed,id
(default: title,items,duedate)
(default: "title,items,duedate")
**--limit, --lm**="": specify limit of items per page (default: 30)
@@ -567,7 +567,7 @@ List milestones of the repository
**--fields, -f**="": Comma-separated list of fields to print. Available values:
title,state,items_open,items_closed,items,duedate,description,created,updated,closed,id
(default: title,items,duedate)
(default: "title,items,duedate")
**--limit, --lm**="": specify limit of items per page (default: 30)
@@ -647,7 +647,7 @@ manage issue/pull of an milestone
**--fields, -f**="": Comma-separated list of fields to print. Available values:
index,state,kind,author,author-id,url,title,body,created,updated,deadline,assignees,milestone,labels,comments,owner,repo
(default: index,kind,title,state,updated,labels)
(default: "index,kind,title,state,updated,labels")
**--kind**="": Filter by kind (issue|pull)
@@ -721,7 +721,7 @@ List Releases
Create a release
**--asset, -a**="": Path to file attachment. Can be specified multiple times (default: [])
**--asset, -a**="": Path to file attachment. Can be specified multiple times
**--draft, -d**: Is a draft
@@ -987,7 +987,7 @@ Show repository details
**--fields, -f**="": Comma-separated list of fields to print. Available values:
description,forks,id,name,owner,stars,ssh,updated,url,permission,type
(default: owner,name,type,ssh)
(default: "owner,name,type,ssh")
**--limit, --lm**="": specify limit of items per page (default: 30)
@@ -1009,7 +1009,7 @@ List repositories you have access to
**--fields, -f**="": Comma-separated list of fields to print. Available values:
description,forks,id,name,owner,stars,ssh,updated,url,permission,type
(default: owner,name,type,ssh)
(default: "owner,name,type,ssh")
**--limit, --lm**="": specify limit of items per page (default: 30)
@@ -1033,7 +1033,7 @@ Find any repo on an Gitea instance
**--fields, -f**="": Comma-separated list of fields to print. Available values:
description,forks,id,name,owner,stars,ssh,updated,url,permission,type
(default: owner,name,type,ssh)
(default: "owner,name,type,ssh")
**--limit, --lm**="": specify limit of items per page (default: 30)
@@ -1195,7 +1195,7 @@ Consult branches
**--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)
(default: "name,protected,user-can-merge,user-can-push")
**--limit, --lm**="": specify limit of items per page (default: 30)
@@ -1215,7 +1215,7 @@ List branches of the repository
**--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)
(default: "name,protected,user-can-merge,user-can-push")
**--limit, --lm**="": specify limit of items per page (default: 30)
@@ -1235,7 +1235,7 @@ Protect branches
**--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)
(default: "name,protected,user-can-merge,user-can-push")
**--limit, --lm**="": specify limit of items per page (default: 30)
@@ -1255,7 +1255,7 @@ Unprotect branches
**--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)
(default: "name,protected,user-can-merge,user-can-push")
**--limit, --lm**="": specify limit of items per page (default: 30)
@@ -1287,10 +1287,14 @@ Manage repository action secrets
List action secrets
**--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
@@ -1373,12 +1377,54 @@ Delete an action variable
**--repo, -r**="": Override local repository path or gitea repository slug to interact with. Optional
### runs, run
List workflow runs
**--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
### jobs, job
List jobs for a workflow run
**--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
### logs, log
Display logs for a job
**--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
## webhooks, webhook, hooks, hook
Manage webhooks
**--global**: operate on global webhooks
**--limit, --lm**="": specify limit of items per page (default: 30)
**--login**="": gitea login instance to use
**--login, -l**="": Use a different Gitea Login. Optional
@@ -1389,6 +1435,8 @@ Manage webhooks
**--output, -o**="": output format [table, csv, simple, tsv, yaml, json]
**--page, -p**="": specify page (default: 1)
**--remote, -R**="": Discover Gitea login from remote. Optional
**--repo**="": repository to operate on
@@ -1399,10 +1447,14 @@ Manage webhooks
List webhooks
**--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
@@ -1417,7 +1469,7 @@ Create a webhook
**--branch-filter**="": branch filter for push events
**--events**="": comma separated list of events (default: push)
**--events**="": comma separated list of events (default: "push")
**--login, -l**="": Use a different Gitea Login. Optional
@@ -1429,7 +1481,7 @@ Create a webhook
**--secret**="": webhook secret
**--type**="": webhook type (gitea, gogs, slack, discord, dingtalk, telegram, msteams, feishu, wechatwork, packagist) (default: gitea)
**--type**="": webhook type (gitea, gogs, slack, discord, dingtalk, telegram, msteams, feishu, wechatwork, packagist) (default: "gitea")
### delete, rm
@@ -1499,7 +1551,7 @@ Show notifications
**--fields, -f**="": Comma-separated list of fields to print. Available values:
id,status,updated,index,type,state,title,repository
(default: id,status,index,type,state,title)
(default: "id,status,index,type,state,title")
**--limit, --lm**="": specify limit of items per page (default: 30)
@@ -1517,7 +1569,7 @@ Show notifications
**--states, -s**="": Comma-separated list of notification states to filter by. Available values:
pinned,unread,read
(default: unread,pinned)
(default: "unread,pinned")
**--types, -t**="": Comma-separated list of subject types to filter by. Available values:
issue,pull,repository,commit
@@ -1529,7 +1581,7 @@ List notifications
**--fields, -f**="": Comma-separated list of fields to print. Available values:
id,status,updated,index,type,state,title,repository
(default: id,status,index,type,state,title)
(default: "id,status,index,type,state,title")
**--limit, --lm**="": specify limit of items per page (default: 30)
@@ -1547,7 +1599,7 @@ List notifications
**--states, -s**="": Comma-separated list of notification states to filter by. Available values:
pinned,unread,read
(default: unread,pinned)
(default: "unread,pinned")
**--types, -t**="": Comma-separated list of subject types to filter by. Available values:
issue,pull,repository,commit
@@ -1573,7 +1625,7 @@ Mark all filtered or a specific notification as read
**--states, -s**="": Comma-separated list of notification states to filter by. Available values:
pinned,unread,read
(default: unread,pinned)
(default: "unread,pinned")
### unread, u
@@ -1595,7 +1647,7 @@ Mark all filtered or a specific notification as unread
**--states, -s**="": Comma-separated list of notification states to filter by. Available values:
pinned,unread,read
(default: unread,pinned)
(default: "unread,pinned")
### pin, p
@@ -1617,7 +1669,7 @@ Mark all filtered or a specific notification as pinned
**--states, -s**="": Comma-separated list of notification states to filter by. Available values:
pinned,unread,read
(default: unread,pinned)
(default: "unread,pinned")
### unpin
@@ -1639,7 +1691,7 @@ Unpin all pinned or a specific notification
**--states, -s**="": Comma-separated list of notification states to filter by. Available values:
pinned,unread,read
(default: unread,pinned)
(default: "unread,pinned")
## clone, C
@@ -1659,7 +1711,7 @@ Manage registered users
**--fields, -f**="": Comma-separated list of fields to print. Available values:
id,login,full_name,email,avatar_url,language,is_admin,restricted,prohibit_login,location,website,description,visibility,activated,lastlogin_at,created_at
(default: id,login,full_name,email,activated)
(default: "id,login,full_name,email,activated")
**--limit, --lm**="": specify limit of items per page (default: 30)
@@ -1679,7 +1731,7 @@ List Users
**--fields, -f**="": Comma-separated list of fields to print. Available values:
id,login,full_name,email,avatar_url,language,is_admin,restricted,prohibit_login,location,website,description,visibility,activated,lastlogin_at,created_at
(default: id,login,full_name,email,activated)
(default: "id,login,full_name,email,activated")
**--limit, --lm**="": specify limit of items per page (default: 30)

48
go.mod
View File

@@ -1,29 +1,27 @@
module code.gitea.io/tea
go 1.23.0
toolchain go1.24.4
go 1.24.4
require (
code.gitea.io/gitea-vet v0.2.3
code.gitea.io/sdk/gitea v0.22.0
code.gitea.io/sdk/gitea v0.22.1
gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c
github.com/adrg/xdg v0.5.3
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/charmbracelet/glamour v0.10.0
github.com/charmbracelet/huh v0.7.0
github.com/charmbracelet/huh v0.8.0
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
github.com/enescakir/emoji v1.0.0
github.com/go-git/go-git/v5 v5.16.2
github.com/go-git/go-git/v5 v5.16.4
github.com/muesli/termenv v0.16.0
github.com/olekukonko/tablewriter v1.0.7
github.com/olekukonko/tablewriter v1.1.1
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/stretchr/testify v1.10.0
github.com/urfave/cli-docs/v3 v3.0.0-alpha6
github.com/urfave/cli/v3 v3.3.8
golang.org/x/crypto v0.39.0
golang.org/x/oauth2 v0.30.0
golang.org/x/term v0.32.0
github.com/stretchr/testify v1.11.1
github.com/urfave/cli-docs/v3 v3.1.0
github.com/urfave/cli/v3 v3.6.1
golang.org/x/crypto v0.45.0
golang.org/x/oauth2 v0.33.0
golang.org/x/term v0.37.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -37,14 +35,17 @@ require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/bubbles v0.21.0 // indirect
github.com/charmbracelet/bubbletea v1.3.5 // indirect
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
github.com/charmbracelet/bubbletea v1.3.6 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/ansi v0.9.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/clipperhouse/displaywidth v0.3.1 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
@@ -67,14 +68,15 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.8 // indirect
github.com/olekukonko/ll v0.1.2 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
@@ -85,11 +87,11 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yuin/goldmark v1.7.8 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/tools v0.33.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/tools v0.38.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

96
go.sum
View File

@@ -1,7 +1,7 @@
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.22.0 h1:HCKq7bX/HQ85Nw7c/HAhWgRye+vBp5nQOE8Md1+9Ef0=
code.gitea.io/sdk/gitea v0.22.0/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
code.gitea.io/sdk/gitea v0.22.1 h1:7K05KjRORyTcTYULQ/AwvlVS6pawLcWyXZcTr7gHFyA=
code.gitea.io/sdk/gitea v0.22.1/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c h1:8fTkq2UaVkLHZCF+iB4wTxINmVAToe2geZGayk9LMbA=
@@ -33,26 +33,26 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc=
github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
github.com/charmbracelet/huh v0.7.0 h1:W8S1uyGETgj9Tuda3/JdVkc3x7DBLZYPZc4c+/rnRdc=
github.com/charmbracelet/huh v0.7.0/go.mod h1:UGC3DZHlgOKHvHC07a5vHag41zzhpPFj34U92sOmyuk=
github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY=
github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
@@ -71,6 +71,12 @@ github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
github.com/clipperhouse/displaywidth v0.3.1 h1:k07iN9gD32177o1y4O1jQMzbLdCrsGJh+blirVYybsk=
github.com/clipperhouse/displaywidth v0.3.1/go.mod h1:tgLJKKyaDOCadywag3agw4snxS5kYEuYR6Y9+qWDDYM=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
@@ -108,8 +114,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
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.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
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=
@@ -141,8 +147,8 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/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=
@@ -155,12 +161,14 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc=
github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=
github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
github.com/olekukonko/ll v0.1.2 h1:lkg/k/9mlsy0SxO5aC+WEpbdT5K83ddnNhAepz7TQc0=
github.com/olekukonko/ll v0.1.2/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
github.com/olekukonko/tablewriter v1.1.1 h1:b3reP6GCfrHwmKkYwNRFh2rxidGHcT6cgxj/sHiDDx0=
github.com/olekukonko/tablewriter v1.1.1/go.mod h1:De/bIcTF+gpBDB3Alv3fEsZA+9unTsSzAg/ZGADCtn4=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
@@ -191,12 +199,12 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli-docs/v3 v3.0.0-alpha6 h1:w/l/N0xw1rO/aHRIGXJ0lDwwYFOzilup1qGvIytP3BI=
github.com/urfave/cli-docs/v3 v3.0.0-alpha6/go.mod h1:p7Z4lg8FSTrPB9GTaNyTrK3ygffHZcK3w0cU2VE+mzU=
github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E=
github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
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.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo=
github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
@@ -212,26 +220,26 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
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.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
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.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
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.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -242,21 +250,21 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
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.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
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.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
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=

99
modules/api/client.go Normal file
View File

@@ -0,0 +1,99 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package api
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"code.gitea.io/tea/modules/config"
)
// Client provides methods for making raw API calls to Gitea
type Client struct {
login *config.Login
httpClient *http.Client
}
// NewClient creates a new API client from a login
func NewClient(login *config.Login) *Client {
httpClient := &http.Client{}
if login.Insecure {
httpClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
}
return &Client{
login: login,
httpClient: httpClient,
}
}
// Get makes an authenticated GET request to the API and decodes the JSON response
func (c *Client) Get(ctx context.Context, path string, result interface{}) (*http.Response, error) {
url := fmt.Sprintf("%s/api/v1%s", c.login.URL, path)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "token "+c.login.Token)
req.Header.Set("Accept", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
return resp, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
if result != nil {
if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
return resp, fmt.Errorf("failed to decode response: %w", err)
}
}
return resp, nil
}
// GetRaw makes an authenticated GET request and returns the raw response body
func (c *Client) GetRaw(ctx context.Context, path string) ([]byte, error) {
url := fmt.Sprintf("%s/api/v1%s", c.login.URL, path)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "token "+c.login.Token)
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to execute request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
return body, nil
}

203
modules/api/client_test.go Normal file
View File

@@ -0,0 +1,203 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package api
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"code.gitea.io/tea/modules/config"
)
func TestNewClient(t *testing.T) {
login := &config.Login{
URL: "https://gitea.example.com",
Token: "test-token",
Insecure: false,
}
client := NewClient(login)
if client == nil {
t.Fatal("NewClient returned nil")
}
if client.login != login {
t.Error("Client login not set correctly")
}
if client.httpClient == nil {
t.Error("Client httpClient not set")
}
}
func TestNewClientInsecure(t *testing.T) {
login := &config.Login{
URL: "https://gitea.example.com",
Token: "test-token",
Insecure: true,
}
client := NewClient(login)
if client == nil {
t.Fatal("NewClient returned nil")
}
// Verify that insecure transport is configured
if client.httpClient.Transport == nil {
t.Error("Expected custom transport for insecure client")
}
}
func TestGet(t *testing.T) {
// Create a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request
if r.Method != "GET" {
t.Errorf("Expected GET request, got %s", r.Method)
}
if r.Header.Get("Authorization") != "token test-token" {
t.Errorf("Expected authorization header, got %s", r.Header.Get("Authorization"))
}
if r.Header.Get("Accept") != "application/json" {
t.Errorf("Expected accept header, got %s", r.Header.Get("Accept"))
}
// Return test response
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}))
defer server.Close()
login := &config.Login{
URL: server.URL,
Token: "test-token",
}
client := NewClient(login)
var result map[string]string
_, err := client.Get(context.Background(), "/test", &result)
if err != nil {
t.Fatalf("Get returned error: %v", err)
}
if result["status"] != "ok" {
t.Errorf("Expected status 'ok', got %s", result["status"])
}
}
func TestGetError(t *testing.T) {
// Create a test server that returns an error
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("not found"))
}))
defer server.Close()
login := &config.Login{
URL: server.URL,
Token: "test-token",
}
client := NewClient(login)
var result map[string]string
_, err := client.Get(context.Background(), "/test", &result)
if err == nil {
t.Fatal("Expected error for 404 response")
}
expectedError := "API request failed with status 404"
if err.Error()[:len(expectedError)] != expectedError {
t.Errorf("Expected error starting with '%s', got '%s'", expectedError, err.Error())
}
}
func TestGetRaw(t *testing.T) {
expectedBody := "raw log content here"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
t.Errorf("Expected GET request, got %s", r.Method)
}
if r.Header.Get("Authorization") != "token test-token" {
t.Errorf("Expected authorization header, got %s", r.Header.Get("Authorization"))
}
w.Write([]byte(expectedBody))
}))
defer server.Close()
login := &config.Login{
URL: server.URL,
Token: "test-token",
}
client := NewClient(login)
body, err := client.GetRaw(context.Background(), "/logs")
if err != nil {
t.Fatalf("GetRaw returned error: %v", err)
}
if string(body) != expectedBody {
t.Errorf("Expected body '%s', got '%s'", expectedBody, string(body))
}
}
func TestGetRawError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("internal error"))
}))
defer server.Close()
login := &config.Login{
URL: server.URL,
Token: "test-token",
}
client := NewClient(login)
_, err := client.GetRaw(context.Background(), "/logs")
if err == nil {
t.Fatal("Expected error for 500 response")
}
expectedError := "API request failed with status 500"
if err.Error()[:len(expectedError)] != expectedError {
t.Errorf("Expected error starting with '%s', got '%s'", expectedError, err.Error())
}
}
func TestGetWithContextCancellation(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// This should not be reached if context is cancelled
w.Write([]byte("ok"))
}))
defer server.Close()
login := &config.Login{
URL: server.URL,
Token: "test-token",
}
client := NewClient(login)
// Create a cancelled context
ctx, cancel := context.WithCancel(context.Background())
cancel()
var result map[string]string
_, err := client.Get(ctx, "/test", &result)
if err == nil {
t.Fatal("Expected error for cancelled context")
}
}

59
modules/api/types.go Normal file
View File

@@ -0,0 +1,59 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package api
import "time"
// ActionRun represents a workflow run
type ActionRun struct {
ID int64 `json:"id"`
Title string `json:"display_title"`
Path string `json:"path"`
Status string `json:"status"`
Conclusion string `json:"conclusion"`
Event string `json:"event"`
HeadBranch string `json:"head_branch"`
HeadSHA string `json:"head_sha"`
RunNumber int64 `json:"run_number"`
RunAttempt int64 `json:"run_attempt"`
HTMLURL string `json:"html_url"`
URL string `json:"url"`
StartedAt *time.Time `json:"started_at"`
CompletedAt *time.Time `json:"completed_at"`
}
// ActionRunList is a list of workflow runs
type ActionRunList struct {
TotalCount int64 `json:"total_count"`
WorkflowRuns []*ActionRun `json:"workflow_runs"`
}
// ActionJob represents a job within a workflow run
type ActionJob struct {
ID int64 `json:"id"`
RunID int64 `json:"run_id"`
Name string `json:"name"`
Status string `json:"status"`
Conclusion string `json:"conclusion"`
HTMLURL string `json:"html_url"`
StartedAt *time.Time `json:"started_at"`
CompletedAt *time.Time `json:"completed_at"`
Steps []*ActionJobStep `json:"steps"`
}
// ActionJobStep represents a step within a job
type ActionJobStep struct {
Name string `json:"name"`
Number int64 `json:"number"`
Status string `json:"status"`
Conclusion string `json:"conclusion"`
StartedAt *time.Time `json:"started_at"`
CompletedAt *time.Time `json:"completed_at"`
}
// ActionJobList is a list of jobs
type ActionJobList struct {
TotalCount int64 `json:"total_count"`
Jobs []*ActionJob `json:"jobs"`
}

View File

@@ -104,5 +104,5 @@ func saveConfig() error {
if err != nil {
return err
}
return os.WriteFile(ymlPath, bs, 0o660)
return os.WriteFile(ymlPath, bs, 0o600)
}

View File

@@ -282,23 +282,13 @@ func (l *Login) Client(options ...gitea.ClientOption) *gitea.Client {
options = append(options, gitea.SetDebugMode())
}
if ok, err := utils.IsKeyEncrypted(l.SSHKey); ok && err == nil && l.SSHPassphrase == "" {
if err := 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)
}
}
if l.SSHCertPrincipal != "" {
l.askForSSHPassphrase()
options = append(options, gitea.UseSSHCert(l.SSHCertPrincipal, l.SSHKey, l.SSHPassphrase))
}
if l.SSHKeyFingerprint != "" {
l.askForSSHPassphrase()
options = append(options, gitea.UseSSHPubkey(l.SSHKeyFingerprint, l.SSHKey, l.SSHPassphrase))
}
@@ -313,6 +303,20 @@ func (l *Login) Client(options ...gitea.ClientOption) *gitea.Client {
return client
}
func (l *Login) askForSSHPassphrase() {
if ok, err := utils.IsKeyEncrypted(l.SSHKey); ok && err == nil && l.SSHPassphrase == "" {
if err := 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)
}
}
}
// GetSSHHost returns SSH host name
func (l *Login) GetSSHHost() string {
if l.SSHHost != "" {

View File

@@ -163,6 +163,8 @@ and then run your command again.`)
os.Exit(1)
}
// Only prompt for confirmation if the fallback login is not explicitly set as default
if !c.Login.Default {
fallback := false
if err := huh.NewConfirm().
Title(fmt.Sprintf("NOTE: no gitea login detected, whether falling back to login '%s'?", c.Login.Name)).
@@ -175,6 +177,7 @@ and then run your command again.`)
os.Exit(1)
}
}
}
// parse reposlug (owner falling back to login owner if reposlug contains only repo name)
c.Owner, c.Repo = utils.GetOwnerAndRepo(c.RepoSlug, c.Login.User)

View File

@@ -25,6 +25,7 @@ func RepoFromPath(path string) (*TeaRepo, error) {
}
repo, err := git.PlainOpenWithOptions(path, &git.PlainOpenOptions{
DetectDotGit: true,
EnableDotGitCommonDir: true, // Enable commondir support for worktrees
})
if err != nil {
return nil, err

63
modules/git/repo_test.go Normal file
View File

@@ -0,0 +1,63 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package git
import (
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestRepoFromPath_Worktree(t *testing.T) {
// Create a temporary directory for test
tmpDir, err := os.MkdirTemp("", "tea-worktree-test-*")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
mainRepoPath := filepath.Join(tmpDir, "main-repo")
worktreePath := filepath.Join(tmpDir, "worktree")
// Initialize main repository
cmd := exec.Command("git", "init", mainRepoPath)
assert.NoError(t, cmd.Run())
// Configure git for the test
cmd = exec.Command("git", "-C", mainRepoPath, "config", "user.email", "test@example.com")
assert.NoError(t, cmd.Run())
cmd = exec.Command("git", "-C", mainRepoPath, "config", "user.name", "Test User")
assert.NoError(t, cmd.Run())
// Add a remote to the main repository
cmd = exec.Command("git", "-C", mainRepoPath, "remote", "add", "origin", "https://gitea.com/owner/repo.git")
assert.NoError(t, cmd.Run())
// Create an initial commit (required for worktree)
readmePath := filepath.Join(mainRepoPath, "README.md")
err = os.WriteFile(readmePath, []byte("# Test Repo\n"), 0644)
assert.NoError(t, err)
cmd = exec.Command("git", "-C", mainRepoPath, "add", "README.md")
assert.NoError(t, cmd.Run())
cmd = exec.Command("git", "-C", mainRepoPath, "commit", "-m", "Initial commit")
assert.NoError(t, cmd.Run())
// Create a worktree
cmd = exec.Command("git", "-C", mainRepoPath, "worktree", "add", worktreePath, "-b", "test-branch")
assert.NoError(t, cmd.Run())
// Test: Open repository from worktree path
repo, err := RepoFromPath(worktreePath)
assert.NoError(t, err, "Should be able to open worktree")
// Test: Read config from worktree (should read from main repo's config)
config, err := repo.Config()
assert.NoError(t, err, "Should be able to read config")
// Verify that remotes are accessible from worktree
assert.NotEmpty(t, config.Remotes, "Should be able to read remotes from worktree")
assert.Contains(t, config.Remotes, "origin", "Should have origin remote")
assert.Equal(t, "https://gitea.com/owner/repo.git", config.Remotes["origin"].URLs[0], "Should have correct remote URL")
}

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"code.gitea.io/sdk/gitea"
"code.gitea.io/tea/modules/api"
)
// ActionSecretsList prints a list of action secrets
@@ -74,3 +75,94 @@ func ActionVariablesList(variables []*gitea.RepoActionVariable, output string) {
t.sort(0, true)
t.print(output)
}
// ActionRunsList prints a list of workflow runs
func ActionRunsList(runs []*api.ActionRun, output string) {
t := table{
headers: []string{
"ID",
"Title",
"Status",
"Conclusion",
"Event",
"Branch",
"Started",
},
}
for _, run := range runs {
conclusion := run.Conclusion
if conclusion == "" {
conclusion = "-"
}
started := ""
if run.StartedAt != nil {
started = FormatTime(*run.StartedAt, output != "")
}
t.addRow(
fmt.Sprintf("%d", run.ID),
run.Title,
run.Status,
conclusion,
run.Event,
run.HeadBranch,
started,
)
}
if len(runs) == 0 {
fmt.Printf("No workflow runs found\n")
return
}
t.print(output)
}
// ActionJobsList prints a list of jobs
func ActionJobsList(jobs []*api.ActionJob, output string) {
t := table{
headers: []string{
"ID",
"Name",
"Status",
"Conclusion",
"Started",
"Completed",
},
}
for _, job := range jobs {
conclusion := job.Conclusion
if conclusion == "" {
conclusion = "-"
}
started := ""
if job.StartedAt != nil {
started = FormatTime(*job.StartedAt, output != "")
}
completed := ""
if job.CompletedAt != nil {
completed = FormatTime(*job.CompletedAt, output != "")
}
t.addRow(
fmt.Sprintf("%d", job.ID),
job.Name,
job.Status,
conclusion,
started,
completed,
)
}
if len(jobs) == 0 {
fmt.Printf("No jobs found\n")
return
}
t.print(output)
}

View File

@@ -29,13 +29,13 @@ func outputMarkdown(markdown string, baseURL string) error {
glamour.WithWordWrap(getWordWrap()),
)
if err != nil {
fmt.Printf(markdown)
fmt.Print(markdown)
return err
}
out, err := renderer.Render(markdown)
if err != nil {
fmt.Printf(markdown)
fmt.Print(markdown)
return err
}
fmt.Print(out)