mirror of
https://gitea.com/gitea/tea.git
synced 2026-04-05 16:03:32 +02:00
replace log.Fatal/os.Exit with error returns (#941)
* Use stdlib encoders * Reduce some duplication * Remove global pagination state * Dedupe JSON detail types * Bump golangci-lint Reviewed-on: https://gitea.com/gitea/tea/pulls/941 Co-authored-by: techknowlogick <techknowlogick@gitea.com> Co-committed-by: techknowlogick <techknowlogick@gitea.com>
This commit is contained in:
committed by
techknowlogick
parent
21881525a8
commit
b05e03416b
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// ActionSecretsList prints a list of action secrets
|
||||
func ActionSecretsList(secrets []*gitea.Secret, output string) {
|
||||
func ActionSecretsList(secrets []*gitea.Secret, output string) error {
|
||||
t := table{
|
||||
headers: []string{
|
||||
"Name",
|
||||
@@ -27,11 +27,11 @@ func ActionSecretsList(secrets []*gitea.Secret, output string) {
|
||||
|
||||
if len(secrets) == 0 {
|
||||
fmt.Printf("No secrets found\n")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
t.sort(0, true)
|
||||
t.print(output)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// ActionVariableDetails prints details of a specific action variable
|
||||
@@ -43,7 +43,7 @@ func ActionVariableDetails(variable *gitea.RepoActionVariable) {
|
||||
}
|
||||
|
||||
// ActionVariablesList prints a list of action variables
|
||||
func ActionVariablesList(variables []*gitea.RepoActionVariable, output string) {
|
||||
func ActionVariablesList(variables []*gitea.RepoActionVariable, output string) error {
|
||||
t := table{
|
||||
headers: []string{
|
||||
"Name",
|
||||
@@ -68,9 +68,9 @@ func ActionVariablesList(variables []*gitea.RepoActionVariable, output string) {
|
||||
|
||||
if len(variables) == 0 {
|
||||
fmt.Printf("No variables found\n")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
t.sort(0, true)
|
||||
t.print(output)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func getWorkflowDisplayName(run *gitea.ActionWorkflowRun) string {
|
||||
}
|
||||
|
||||
// ActionRunsList prints a list of workflow runs
|
||||
func ActionRunsList(runs []*gitea.ActionWorkflowRun, output string) {
|
||||
func ActionRunsList(runs []*gitea.ActionWorkflowRun, output string) error {
|
||||
t := table{
|
||||
headers: []string{
|
||||
"ID",
|
||||
@@ -74,11 +74,11 @@ func ActionRunsList(runs []*gitea.ActionWorkflowRun, output string) {
|
||||
|
||||
if len(runs) == 0 {
|
||||
fmt.Printf("No workflow runs found\n")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
t.sort(0, true)
|
||||
t.print(output)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// ActionRunDetails prints detailed information about a workflow run
|
||||
@@ -114,7 +114,7 @@ func ActionRunDetails(run *gitea.ActionWorkflowRun) {
|
||||
}
|
||||
|
||||
// ActionWorkflowJobsList prints a list of workflow jobs
|
||||
func ActionWorkflowJobsList(jobs []*gitea.ActionWorkflowJob, output string) {
|
||||
func ActionWorkflowJobsList(jobs []*gitea.ActionWorkflowJob, output string) error {
|
||||
t := table{
|
||||
headers: []string{
|
||||
"ID",
|
||||
@@ -147,15 +147,15 @@ func ActionWorkflowJobsList(jobs []*gitea.ActionWorkflowJob, output string) {
|
||||
|
||||
if len(jobs) == 0 {
|
||||
fmt.Printf("No jobs found\n")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
t.sort(0, true)
|
||||
t.print(output)
|
||||
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) {
|
||||
func WorkflowsList(workflows []*gitea.ContentsResponse, activeStatus map[string]bool, output string) error {
|
||||
t := table{
|
||||
headers: []string{
|
||||
"Active",
|
||||
@@ -180,9 +180,9 @@ func WorkflowsList(workflows []*gitea.ContentsResponse, activeStatus map[string]
|
||||
|
||||
if len(workflows) == 0 {
|
||||
fmt.Printf("No workflows found\n")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
t.sort(1, true) // Sort by name column
|
||||
t.print(output)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActionRunsListEmpty(t *testing.T) {
|
||||
@@ -18,7 +19,7 @@ func TestActionRunsListEmpty(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
ActionRunsList([]*gitea.ActionWorkflowRun{}, "")
|
||||
require.NoError(t, ActionRunsList([]*gitea.ActionWorkflowRun{}, ""))
|
||||
}
|
||||
|
||||
func TestActionRunsListWithData(t *testing.T) {
|
||||
@@ -49,7 +50,7 @@ func TestActionRunsListWithData(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
ActionRunsList(runs, "")
|
||||
require.NoError(t, ActionRunsList(runs, ""))
|
||||
}
|
||||
|
||||
func TestActionRunDetails(t *testing.T) {
|
||||
@@ -90,7 +91,7 @@ func TestActionWorkflowJobsListEmpty(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
ActionWorkflowJobsList([]*gitea.ActionWorkflowJob{}, "")
|
||||
require.NoError(t, ActionWorkflowJobsList([]*gitea.ActionWorkflowJob{}, ""))
|
||||
}
|
||||
|
||||
func TestActionWorkflowJobsListWithData(t *testing.T) {
|
||||
@@ -119,7 +120,7 @@ func TestActionWorkflowJobsListWithData(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
ActionWorkflowJobsList(jobs, "")
|
||||
require.NoError(t, ActionWorkflowJobsList(jobs, ""))
|
||||
}
|
||||
|
||||
func TestFormatDurationMinutes(t *testing.T) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActionSecretsListEmpty(t *testing.T) {
|
||||
@@ -21,7 +22,7 @@ func TestActionSecretsListEmpty(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
ActionSecretsList([]*gitea.Secret{}, "")
|
||||
require.NoError(t, ActionSecretsList([]*gitea.Secret{}, ""))
|
||||
}
|
||||
|
||||
func TestActionSecretsListWithData(t *testing.T) {
|
||||
@@ -43,7 +44,7 @@ func TestActionSecretsListWithData(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
ActionSecretsList(secrets, "")
|
||||
require.NoError(t, ActionSecretsList(secrets, ""))
|
||||
|
||||
// Test JSON output format to verify structure
|
||||
var buf bytes.Buffer
|
||||
@@ -55,7 +56,7 @@ func TestActionSecretsListWithData(t *testing.T) {
|
||||
testTable.addRow(secret.Name, FormatTime(secret.Created, true))
|
||||
}
|
||||
|
||||
testTable.fprint(&buf, "json")
|
||||
require.NoError(t, testTable.fprint(&buf, "json"))
|
||||
output := buf.String()
|
||||
|
||||
if !strings.Contains(output, "TEST_SECRET_1") {
|
||||
@@ -92,7 +93,7 @@ func TestActionVariablesListEmpty(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
ActionVariablesList([]*gitea.RepoActionVariable{}, "")
|
||||
require.NoError(t, ActionVariablesList([]*gitea.RepoActionVariable{}, ""))
|
||||
}
|
||||
|
||||
func TestActionVariablesListWithData(t *testing.T) {
|
||||
@@ -118,7 +119,7 @@ func TestActionVariablesListWithData(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
ActionVariablesList(variables, "")
|
||||
require.NoError(t, ActionVariablesList(variables, ""))
|
||||
|
||||
// Test JSON output format to verify structure and truncation
|
||||
var buf bytes.Buffer
|
||||
@@ -134,7 +135,7 @@ func TestActionVariablesListWithData(t *testing.T) {
|
||||
testTable.addRow(variable.Name, value, strconv.Itoa(int(variable.RepoID)))
|
||||
}
|
||||
|
||||
testTable.fprint(&buf, "json")
|
||||
require.NoError(t, testTable.fprint(&buf, "json"))
|
||||
output := buf.String()
|
||||
|
||||
if !strings.Contains(output, "TEST_VARIABLE_1") {
|
||||
@@ -165,7 +166,7 @@ func TestActionVariablesListValueTruncation(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
ActionVariablesList([]*gitea.RepoActionVariable{variable}, "")
|
||||
require.NoError(t, ActionVariablesList([]*gitea.RepoActionVariable{variable}, ""))
|
||||
|
||||
// Test the truncation logic directly
|
||||
value := variable.Value
|
||||
|
||||
@@ -17,7 +17,7 @@ func formatByteSize(size int64) string {
|
||||
}
|
||||
|
||||
// ReleaseAttachmentsList prints a listing of release attachments
|
||||
func ReleaseAttachmentsList(attachments []*gitea.Attachment, output string) {
|
||||
func ReleaseAttachmentsList(attachments []*gitea.Attachment, output string) error {
|
||||
t := tableWithHeader(
|
||||
"Name",
|
||||
"Size",
|
||||
@@ -30,5 +30,5 @@ func ReleaseAttachmentsList(attachments []*gitea.Attachment, output string) {
|
||||
)
|
||||
}
|
||||
|
||||
t.print(output)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
@@ -10,8 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// BranchesList prints a listing of the branches
|
||||
func BranchesList(branches []*gitea.Branch, protections []*gitea.BranchProtection, output string, fields []string) {
|
||||
fmt.Println(fields)
|
||||
func BranchesList(branches []*gitea.Branch, protections []*gitea.BranchProtection, output string, fields []string) error {
|
||||
printables := make([]printable, len(branches))
|
||||
|
||||
for i, branch := range branches {
|
||||
@@ -25,7 +24,7 @@ func BranchesList(branches []*gitea.Branch, protections []*gitea.BranchProtectio
|
||||
}
|
||||
|
||||
t := tableFromItems(fields, printables, isMachineReadable(output))
|
||||
t.print(output)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
type printableBranch struct {
|
||||
@@ -54,17 +53,17 @@ func (x printableBranch) FormatField(field string, machineReadable bool) string
|
||||
}
|
||||
merging := ""
|
||||
for _, entry := range x.protection.MergeWhitelistTeams {
|
||||
approving += entry + "/"
|
||||
merging += entry + "/"
|
||||
}
|
||||
for _, entry := range x.protection.MergeWhitelistUsernames {
|
||||
approving += entry + "/"
|
||||
merging += entry + "/"
|
||||
}
|
||||
pushing := ""
|
||||
for _, entry := range x.protection.PushWhitelistTeams {
|
||||
approving += entry + "/"
|
||||
pushing += entry + "/"
|
||||
}
|
||||
for _, entry := range x.protection.PushWhitelistUsernames {
|
||||
approving += entry + "/"
|
||||
pushing += entry + "/"
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"- enable-push: %t\n- approving: %s\n- merging: %s\n- pushing: %s\n",
|
||||
|
||||
33
modules/print/branch_test.go
Normal file
33
modules/print/branch_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPrintableBranchProtectionUsesSeparateWhitelists(t *testing.T) {
|
||||
protection := &gitea.BranchProtection{
|
||||
EnablePush: true,
|
||||
ApprovalsWhitelistTeams: []string{"approve-team"},
|
||||
ApprovalsWhitelistUsernames: []string{"approve-user"},
|
||||
MergeWhitelistTeams: []string{"merge-team"},
|
||||
MergeWhitelistUsernames: []string{"merge-user"},
|
||||
PushWhitelistTeams: []string{"push-team"},
|
||||
PushWhitelistUsernames: []string{"push-user"},
|
||||
}
|
||||
|
||||
result := printableBranch{
|
||||
branch: &gitea.Branch{Name: "main"},
|
||||
protection: protection,
|
||||
}.FormatField("protection", false)
|
||||
|
||||
assert.Contains(t, result, "- approving: approve-team/approve-user/")
|
||||
assert.Contains(t, result, "- merging: merge-team/merge-user/")
|
||||
assert.Contains(t, result, "- pushing: push-team/push-user/")
|
||||
assert.NotContains(t, result, "- approving: approve-team/approve-user/merge-team/")
|
||||
}
|
||||
@@ -45,8 +45,8 @@ func formatReactions(reactions []*gitea.Reaction) string {
|
||||
}
|
||||
|
||||
// IssuesPullsList prints a listing of issues & pulls
|
||||
func IssuesPullsList(issues []*gitea.Issue, output string, fields []string) {
|
||||
printIssues(issues, output, fields)
|
||||
func IssuesPullsList(issues []*gitea.Issue, output string, fields []string) error {
|
||||
return printIssues(issues, output, fields)
|
||||
}
|
||||
|
||||
// IssueFields are all available fields to print with IssuesList()
|
||||
@@ -73,7 +73,7 @@ var IssueFields = []string{
|
||||
"repo",
|
||||
}
|
||||
|
||||
func printIssues(issues []*gitea.Issue, output string, fields []string) {
|
||||
func printIssues(issues []*gitea.Issue, output string, fields []string) error {
|
||||
labelMap := map[int64]string{}
|
||||
printables := make([]printable, len(issues))
|
||||
machineReadable := isMachineReadable(output)
|
||||
@@ -90,7 +90,7 @@ func printIssues(issues []*gitea.Issue, output string, fields []string) {
|
||||
}
|
||||
|
||||
t := tableFromItems(fields, printables, machineReadable)
|
||||
t.print(output)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
type printableIssue struct {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// LabelsList prints a listing of labels
|
||||
func LabelsList(labels []*gitea.Label, output string) {
|
||||
func LabelsList(labels []*gitea.Label, output string) error {
|
||||
t := tableWithHeader(
|
||||
"Index",
|
||||
"Color",
|
||||
@@ -26,5 +26,5 @@ func LabelsList(labels []*gitea.Label, output string) {
|
||||
label.Description,
|
||||
)
|
||||
}
|
||||
t.print(output)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func LoginDetails(login *config.Login) {
|
||||
}
|
||||
|
||||
// LoginsList prints a listing of logins
|
||||
func LoginsList(logins []config.Login, output string) {
|
||||
func LoginsList(logins []config.Login, output string) error {
|
||||
t := tableWithHeader(
|
||||
"Name",
|
||||
"URL",
|
||||
@@ -50,5 +50,5 @@ func LoginsList(logins []config.Login, output string) {
|
||||
)
|
||||
}
|
||||
|
||||
t.print(output)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
@@ -23,14 +23,14 @@ func MilestoneDetails(milestone *gitea.Milestone) {
|
||||
}
|
||||
|
||||
// MilestonesList prints a listing of milestones
|
||||
func MilestonesList(news []*gitea.Milestone, output string, fields []string) {
|
||||
func MilestonesList(news []*gitea.Milestone, output string, fields []string) error {
|
||||
printables := make([]printable, len(news))
|
||||
for i, x := range news {
|
||||
printables[i] = &printableMilestone{x}
|
||||
}
|
||||
t := tableFromItems(fields, printables, isMachineReadable(output))
|
||||
t.sort(0, true)
|
||||
t.print(output)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// MilestoneFields are all available fields to print with MilestonesList
|
||||
|
||||
@@ -11,13 +11,13 @@ import (
|
||||
)
|
||||
|
||||
// NotificationsList prints a listing of notification threads
|
||||
func NotificationsList(news []*gitea.NotificationThread, output string, fields []string) {
|
||||
func NotificationsList(news []*gitea.NotificationThread, output string, fields []string) error {
|
||||
printables := make([]printable, len(news))
|
||||
for i, x := range news {
|
||||
printables[i] = &printableNotification{x}
|
||||
}
|
||||
t := tableFromItems(fields, printables, isMachineReadable(output))
|
||||
t.print(output)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// NotificationFields are all available fields to print with NotificationsList
|
||||
|
||||
@@ -22,10 +22,10 @@ func OrganizationDetails(org *gitea.Organization) {
|
||||
}
|
||||
|
||||
// OrganizationsList prints a listing of the organizations
|
||||
func OrganizationsList(organizations []*gitea.Organization, output string) {
|
||||
func OrganizationsList(organizations []*gitea.Organization, output string) error {
|
||||
if len(organizations) == 0 {
|
||||
fmt.Println("No organizations found")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
t := tableWithHeader(
|
||||
@@ -46,5 +46,5 @@ func OrganizationsList(organizations []*gitea.Organization, output string) {
|
||||
)
|
||||
}
|
||||
|
||||
t.print(output)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
@@ -138,8 +138,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) {
|
||||
printPulls(prs, output, fields)
|
||||
func PullsList(prs []*gitea.PullRequest, output string, fields []string) error {
|
||||
return printPulls(prs, output, fields)
|
||||
}
|
||||
|
||||
// PullFields are all available fields to print with PullsList()
|
||||
@@ -170,7 +170,7 @@ var PullFields = []string{
|
||||
"comments",
|
||||
}
|
||||
|
||||
func printPulls(pulls []*gitea.PullRequest, output string, fields []string) {
|
||||
func printPulls(pulls []*gitea.PullRequest, output string, fields []string) error {
|
||||
labelMap := map[int64]string{}
|
||||
printables := make([]printable, len(pulls))
|
||||
machineReadable := isMachineReadable(output)
|
||||
@@ -187,7 +187,7 @@ func printPulls(pulls []*gitea.PullRequest, output string, fields []string) {
|
||||
}
|
||||
|
||||
t := tableFromItems(fields, printables, machineReadable)
|
||||
t.print(output)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
type printablePull struct {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// ReleasesList prints a listing of releases
|
||||
func ReleasesList(releases []*gitea.Release, output string) {
|
||||
func ReleasesList(releases []*gitea.Release, output string) error {
|
||||
t := tableWithHeader(
|
||||
"Tag-Name",
|
||||
"Title",
|
||||
@@ -33,5 +33,5 @@ func ReleasesList(releases []*gitea.Release, output string) {
|
||||
)
|
||||
}
|
||||
|
||||
t.print(output)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ import (
|
||||
)
|
||||
|
||||
// ReposList prints a listing of the repos
|
||||
func ReposList(repos []*gitea.Repository, output string, fields []string) {
|
||||
func ReposList(repos []*gitea.Repository, output string, fields []string) error {
|
||||
printables := make([]printable, len(repos))
|
||||
for i, r := range repos {
|
||||
printables[i] = &printableRepo{r}
|
||||
}
|
||||
t := tableFromItems(fields, printables, isMachineReadable(output))
|
||||
t.print(output)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// RepoDetails print an repo formatted to stdout
|
||||
@@ -113,7 +113,7 @@ func (x printableRepo) FormatField(field string, machineReadable bool) string {
|
||||
case "forks":
|
||||
return fmt.Sprintf("%d", x.Forks)
|
||||
case "id":
|
||||
return x.FullName
|
||||
return fmt.Sprintf("%d", x.ID)
|
||||
case "name":
|
||||
return x.Name
|
||||
case "owner":
|
||||
|
||||
33
modules/print/repo_test.go
Normal file
33
modules/print/repo_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/sdk/gitea"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReposListUsesNumericIDField(t *testing.T) {
|
||||
repos := []*gitea.Repository{{
|
||||
ID: 123,
|
||||
Name: "tea",
|
||||
Owner: &gitea.User{
|
||||
UserName: "gitea",
|
||||
},
|
||||
}}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
tbl := tableFromItems([]string{"id", "name"}, []printable{&printableRepo{repos[0]}}, 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)
|
||||
require.Equal(t, "123", result[0]["id"])
|
||||
require.Equal(t, "tea", result[0]["name"])
|
||||
}
|
||||
@@ -4,6 +4,8 @@
|
||||
package print
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -14,6 +16,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// table provides infrastructure to easily print (sorted) lists in different formats
|
||||
@@ -72,34 +75,26 @@ func (t table) Less(i, j int) bool {
|
||||
return t.values[i][t.sortColumn] < t.values[j][t.sortColumn]
|
||||
}
|
||||
|
||||
func (t *table) print(output string) {
|
||||
t.fprint(os.Stdout, output)
|
||||
func (t *table) print(output string) error {
|
||||
return t.fprint(os.Stdout, output)
|
||||
}
|
||||
|
||||
func (t *table) fprint(f io.Writer, output string) {
|
||||
func (t *table) fprint(f io.Writer, output string) error {
|
||||
switch output {
|
||||
case "", "table":
|
||||
outputTable(f, t.headers, t.values)
|
||||
return outputTable(f, t.headers, t.values)
|
||||
case "csv":
|
||||
outputDsv(f, t.headers, t.values, ",")
|
||||
return outputDsv(f, t.headers, t.values, ',')
|
||||
case "simple":
|
||||
outputSimple(f, t.headers, t.values)
|
||||
return outputSimple(f, t.headers, t.values)
|
||||
case "tsv":
|
||||
outputDsv(f, t.headers, t.values, "\t")
|
||||
return outputDsv(f, t.headers, t.values, '\t')
|
||||
case "yml", "yaml":
|
||||
outputYaml(f, t.headers, t.values)
|
||||
return outputYaml(f, t.headers, t.values)
|
||||
case "json":
|
||||
outputJSON(f, t.headers, t.values)
|
||||
return outputJSON(f, t.headers, t.values)
|
||||
default:
|
||||
fmt.Fprintf(f, `"unknown output type '%s', available types are:
|
||||
- csv: comma-separated values
|
||||
- simple: space-separated values
|
||||
- table: auto-aligned table format (default)
|
||||
- tsv: tab-separated values
|
||||
- yaml: YAML format
|
||||
- json: JSON format
|
||||
`, output)
|
||||
os.Exit(1)
|
||||
return fmt.Errorf("unknown output type %q, available types are: csv, simple, table, tsv, yaml, json", output)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,41 +113,59 @@ func outputTable(f io.Writer, headers []string, values [][]string) error {
|
||||
}
|
||||
|
||||
// outputSimple prints structured data as space delimited value
|
||||
func outputSimple(f io.Writer, headers []string, values [][]string) {
|
||||
func outputSimple(f io.Writer, headers []string, values [][]string) error {
|
||||
for _, value := range values {
|
||||
fmt.Fprint(f, strings.Join(value, " "))
|
||||
fmt.Fprintf(f, "\n")
|
||||
if _, err := fmt.Fprintln(f, strings.Join(value, " ")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// outputDsv prints structured data as delimiter separated value format
|
||||
func outputDsv(f io.Writer, headers []string, values [][]string, delimiterOpt ...string) {
|
||||
delimiter := ","
|
||||
if len(delimiterOpt) > 0 {
|
||||
delimiter = delimiterOpt[0]
|
||||
// outputDsv prints structured data as delimiter separated value format.
|
||||
func outputDsv(f io.Writer, headers []string, values [][]string, delimiter rune) error {
|
||||
writer := csv.NewWriter(f)
|
||||
writer.Comma = delimiter
|
||||
if err := writer.Write(headers); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(f, "\""+strings.Join(headers, "\""+delimiter+"\"")+"\"")
|
||||
for _, value := range values {
|
||||
fmt.Fprintf(f, "\"")
|
||||
fmt.Fprint(f, strings.Join(value, "\""+delimiter+"\""))
|
||||
fmt.Fprintf(f, "\"")
|
||||
fmt.Fprintf(f, "\n")
|
||||
if err := writer.Write(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
writer.Flush()
|
||||
return writer.Error()
|
||||
}
|
||||
|
||||
// outputYaml prints structured data as yaml
|
||||
func outputYaml(f io.Writer, headers []string, values [][]string) {
|
||||
func outputYaml(f io.Writer, headers []string, values [][]string) error {
|
||||
root := &yaml.Node{Kind: yaml.SequenceNode}
|
||||
for _, value := range values {
|
||||
fmt.Fprintln(f, "-")
|
||||
row := &yaml.Node{Kind: yaml.MappingNode}
|
||||
for j, val := range value {
|
||||
row.Content = append(row.Content, &yaml.Node{
|
||||
Kind: yaml.ScalarNode,
|
||||
Value: headers[j],
|
||||
})
|
||||
|
||||
valueNode := &yaml.Node{Kind: yaml.ScalarNode, Value: val}
|
||||
intVal, _ := strconv.Atoi(val)
|
||||
if strconv.Itoa(intVal) == val {
|
||||
fmt.Fprintf(f, " %s: %s\n", headers[j], val)
|
||||
valueNode.Tag = "!!int"
|
||||
} else {
|
||||
fmt.Fprintf(f, " %s: '%s'\n", headers[j], strings.ReplaceAll(val, "'", "''"))
|
||||
valueNode.Tag = "!!str"
|
||||
}
|
||||
row.Content = append(row.Content, valueNode)
|
||||
}
|
||||
root.Content = append(root.Content, row)
|
||||
}
|
||||
encoder := yaml.NewEncoder(f)
|
||||
if err := encoder.Encode(root); err != nil {
|
||||
_ = encoder.Close()
|
||||
return err
|
||||
}
|
||||
return encoder.Close()
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -166,42 +179,52 @@ func toSnakeCase(str string) string {
|
||||
return strings.ToLower(snake)
|
||||
}
|
||||
|
||||
// outputJSON prints structured data as json
|
||||
// Since golang's map is unordered, we need to ensure consistent ordering, we have
|
||||
// to output the JSON ourselves.
|
||||
func outputJSON(f io.Writer, headers []string, values [][]string) {
|
||||
fmt.Fprintln(f, "[")
|
||||
itemCount := len(values)
|
||||
headersCount := len(headers)
|
||||
const space = " "
|
||||
for i, value := range values {
|
||||
fmt.Fprintf(f, "%s{\n", space)
|
||||
for j, val := range value {
|
||||
v, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to format JSON for value '%s': %v\n", val, err)
|
||||
return
|
||||
}
|
||||
key, err := json.Marshal(toSnakeCase(headers[j]))
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to format JSON for header '%s': %v\n", headers[j], err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(f, "%s:%s", key, v)
|
||||
if j != headersCount-1 {
|
||||
fmt.Fprintln(f, ",")
|
||||
} else {
|
||||
fmt.Fprintln(f)
|
||||
}
|
||||
}
|
||||
// orderedRow preserves header insertion order when marshaled to JSON.
|
||||
type orderedRow struct {
|
||||
keys []string
|
||||
values map[string]string
|
||||
}
|
||||
|
||||
if i != itemCount-1 {
|
||||
fmt.Fprintf(f, "%s},\n", space)
|
||||
} else {
|
||||
fmt.Fprintf(f, "%s}\n", space)
|
||||
func (o orderedRow) MarshalJSON() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte('{')
|
||||
for i, k := range o.keys {
|
||||
if i > 0 {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
key, err := json.Marshal(k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val, err := json.Marshal(o.values[k])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf.Write(key)
|
||||
buf.WriteByte(':')
|
||||
buf.Write(val)
|
||||
}
|
||||
fmt.Fprintln(f, "]")
|
||||
buf.WriteByte('}')
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// outputJSON prints structured data as json, preserving header field order.
|
||||
func outputJSON(f io.Writer, headers []string, values [][]string) error {
|
||||
snakeHeaders := make([]string, len(headers))
|
||||
for i, h := range headers {
|
||||
snakeHeaders[i] = toSnakeCase(h)
|
||||
}
|
||||
rows := make([]orderedRow, 0, len(values))
|
||||
for _, value := range values {
|
||||
row := orderedRow{keys: snakeHeaders, values: make(map[string]string, len(headers))}
|
||||
for j, val := range value {
|
||||
row.values[snakeHeaders[j]] = val
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
encoder := json.NewEncoder(f)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(rows)
|
||||
}
|
||||
|
||||
func isMachineReadable(outputFormat string) bool {
|
||||
|
||||
@@ -5,10 +5,14 @@ package print
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestToSnakeCase(t *testing.T) {
|
||||
@@ -29,7 +33,7 @@ func TestPrint(t *testing.T) {
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
tData.fprint(buf, "json")
|
||||
require.NoError(t, tData.fprint(buf, "json"))
|
||||
result := []struct {
|
||||
A string
|
||||
B string
|
||||
@@ -51,22 +55,62 @@ func TestPrint(t *testing.T) {
|
||||
|
||||
buf.Reset()
|
||||
|
||||
tData.fprint(buf, "yaml")
|
||||
require.NoError(t, tData.fprint(buf, "yaml"))
|
||||
|
||||
assert.Equal(t, `-
|
||||
A: 'new a'
|
||||
B: 'some bbbb'
|
||||
-
|
||||
A: 'AAAAA'
|
||||
B: 'b2'
|
||||
-
|
||||
A: '"abc'
|
||||
B: '"def'
|
||||
-
|
||||
A: '''abc'
|
||||
B: 'de''f'
|
||||
-
|
||||
A: '\abc'
|
||||
B: '''def\'
|
||||
`, buf.String())
|
||||
var yamlResult []map[string]string
|
||||
require.NoError(t, yaml.Unmarshal(buf.Bytes(), &yamlResult))
|
||||
assert.Equal(t, []map[string]string{
|
||||
{"A": "new a", "B": "some bbbb"},
|
||||
{"A": "AAAAA", "B": "b2"},
|
||||
{"A": "\"abc", "B": "\"def"},
|
||||
{"A": "'abc", "B": "de'f"},
|
||||
{"A": "\\abc", "B": "'def\\"},
|
||||
}, yamlResult)
|
||||
}
|
||||
|
||||
func TestPrintCSVUsesEscaping(t *testing.T) {
|
||||
tData := &table{
|
||||
headers: []string{"A", "B"},
|
||||
values: [][]string{
|
||||
{"hello,world", `quote "here"`},
|
||||
{"multi\nline", "plain"},
|
||||
},
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
require.NoError(t, tData.fprint(buf, "csv"))
|
||||
|
||||
reader := csv.NewReader(bytes.NewReader(buf.Bytes()))
|
||||
records, err := reader.ReadAll()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, [][]string{
|
||||
{"A", "B"},
|
||||
{"hello,world", `quote "here"`},
|
||||
{"multi\nline", "plain"},
|
||||
}, records)
|
||||
}
|
||||
|
||||
func TestPrintJSONPreservesFieldOrder(t *testing.T) {
|
||||
tData := &table{
|
||||
headers: []string{"Zebra", "Apple", "Mango"},
|
||||
values: [][]string{{"z", "a", "m"}},
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
require.NoError(t, tData.fprint(buf, "json"))
|
||||
|
||||
// Keys must appear in header order (Zebra, Apple, Mango), not sorted alphabetically
|
||||
raw := buf.String()
|
||||
zebraIdx := bytes.Index([]byte(raw), []byte(`"zebra"`))
|
||||
appleIdx := bytes.Index([]byte(raw), []byte(`"apple"`))
|
||||
mangoIdx := bytes.Index([]byte(raw), []byte(`"mango"`))
|
||||
assert.Greater(t, appleIdx, zebraIdx, "apple should appear after zebra")
|
||||
assert.Greater(t, mangoIdx, appleIdx, "mango should appear after apple")
|
||||
}
|
||||
|
||||
func TestPrintUnknownOutputReturnsError(t *testing.T) {
|
||||
tData := &table{headers: []string{"A"}, values: [][]string{{"value"}}}
|
||||
|
||||
err := tData.fprint(io.Discard, "unknown")
|
||||
require.ErrorContains(t, err, `unknown output type "unknown"`)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// TrackedTimesList print list of tracked times to stdout
|
||||
func TrackedTimesList(times []*gitea.TrackedTime, outputType string, fields []string, printTotal bool) {
|
||||
func TrackedTimesList(times []*gitea.TrackedTime, outputType string, fields []string, printTotal bool) error {
|
||||
printables := make([]printable, len(times))
|
||||
var totalDuration int64
|
||||
for i, t := range times {
|
||||
@@ -26,7 +26,7 @@ func TrackedTimesList(times []*gitea.TrackedTime, outputType string, fields []st
|
||||
t.addRowSlice(total)
|
||||
}
|
||||
|
||||
t.print(outputType)
|
||||
return t.print(outputType)
|
||||
}
|
||||
|
||||
// TrackedTimeFields contains all available fields for printing of tracked times.
|
||||
|
||||
@@ -52,13 +52,13 @@ func UserDetails(user *gitea.User) {
|
||||
}
|
||||
|
||||
// UserList prints a listing of the users
|
||||
func UserList(user []*gitea.User, output string, fields []string) {
|
||||
func UserList(user []*gitea.User, output string, fields []string) error {
|
||||
printables := make([]printable, len(user))
|
||||
for i, u := range user {
|
||||
printables[i] = &printableUser{u}
|
||||
}
|
||||
t := tableFromItems(fields, printables, isMachineReadable(output))
|
||||
t.print(output)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// UserFields are the available fields to print with UserList()
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
// WebhooksList prints a listing of webhooks
|
||||
func WebhooksList(hooks []*gitea.Hook, output string) {
|
||||
func WebhooksList(hooks []*gitea.Hook, output string) error {
|
||||
t := tableWithHeader(
|
||||
"ID",
|
||||
"Type",
|
||||
@@ -48,7 +48,7 @@ func WebhooksList(hooks []*gitea.Hook, output string) {
|
||||
)
|
||||
}
|
||||
|
||||
t.print(output)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// WebhookDetails prints detailed information about a webhook
|
||||
|
||||
@@ -51,10 +51,7 @@ func TestWebhooksList(t *testing.T) {
|
||||
|
||||
for _, format := range outputFormats {
|
||||
t.Run("Format_"+format, func(t *testing.T) {
|
||||
// Should not panic
|
||||
assert.NotPanics(t, func() {
|
||||
WebhooksList(hooks, format)
|
||||
})
|
||||
assert.NoError(t, WebhooksList(hooks, format))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -63,16 +60,12 @@ func TestWebhooksListEmpty(t *testing.T) {
|
||||
// Test with empty hook list
|
||||
hooks := []*gitea.Hook{}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
WebhooksList(hooks, "table")
|
||||
})
|
||||
assert.NoError(t, WebhooksList(hooks, "table"))
|
||||
}
|
||||
|
||||
func TestWebhooksListNil(t *testing.T) {
|
||||
// Test with nil hook list
|
||||
assert.NotPanics(t, func() {
|
||||
WebhooksList(nil, "table")
|
||||
})
|
||||
assert.NoError(t, WebhooksList(nil, "table"))
|
||||
}
|
||||
|
||||
func TestWebhookDetails(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user