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:
techknowlogick
2026-03-27 03:36:44 +00:00
committed by techknowlogick
parent 21881525a8
commit b05e03416b
124 changed files with 1610 additions and 759 deletions

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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",

View 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/")
}

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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":

View 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"])
}

View File

@@ -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 {

View File

@@ -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"`)
}

View File

@@ -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.

View File

@@ -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()

View File

@@ -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

View File

@@ -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) {