mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
feat!: rebuild tea on Cobra with explicit noun/verb commands (#1126)
- **switch tea to the Cobra command tree:** `internal/app` wires the packages from parts 1–4 into explicit noun/verb commands with `--repo`, `--json` / `--jq`, `--sudo` and `--debug`; `main.go` points at it. Build, release and docs tooling (`tools/cli-docs`, Makefile, goreleaser, snap, CI) and README / CONTRIBUTING are updated. - **remove the urfave/cli command tree:** deletes `cmd/`, `modules/`, the old integration tests, contrib completion scripts and the generated `docs/CLI.md`, and drops their dependencies. This commit only deletes. ## Breaking changes This is a breaking release. It requires Gitea 28 or newer; configuration moves to separate `config.yml` and `hosts.yml` files and requires fresh authentication, and command names, aliases, flags and output formats change. `docs/migration-1.0.md` maps every old command, flag and environment variable to its replacement. ## Issues fixes #1118 fixes #1116 fixes #1115 fixes #1094 fixes #1091 fixes #1073 fixes #1052 fixes #1032 fixes #1023 fixes #966 fixes #908 fixes #833 fixes #831 fixes #817 fixes #804 fixes #779 fixes #770 fixes #737 fixes #697 fixes #726 fixes #677 fixes #672 fixes #633 fixes #632 fixes #560 fixes #556 fixes #523 fixes #516 fixes #510 fixes #504 fixes #500 fixes #482 fixes #406 fixes #403 fixes #373 fixes #351 fixes #344 fixes #326 fixes #325 fixes #323 fixes #246 fixes #240 fixes #232 fixes #197 fixes #172 fixes #141 fixes #69 fixes #1110 fixes #569 fixes #480 fixes #666 fixes #670 fixes #457 fixes #643 fixes #498 fixes #490 fixes #1069 fixes #834 fixes #602 fixes #328 fixes #417 fixes #571 fixes #1029 fixes #489 fixes #1050 fixes #162 fixes #430 fixes #1087 fixes #683 Reviewed-on: https://gitea.com/gitea/tea/pulls/1126 Reviewed-by: TheFox0x7 <95654+thefox0x7@noreply.gitea.com>
This commit is contained in:
1 parent
12e415a7a7
commit
f02245449b
456 files changed
+18892
-34015
No files matched your search
@@ -1,76 +0,0 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// ActionSecretsList prints a list of action secrets
|
||||
func ActionSecretsList(secrets []*gitea.Secret, output string) error {
|
||||
t := table{
|
||||
headers: []string{
|
||||
"Name",
|
||||
"Created",
|
||||
},
|
||||
}
|
||||
|
||||
for _, secret := range secrets {
|
||||
t.addRow(
|
||||
secret.Name,
|
||||
FormatTime(secret.Created, output != ""),
|
||||
)
|
||||
}
|
||||
|
||||
if len(secrets) == 0 {
|
||||
fmt.Printf("No secrets found\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
t.sort(0, true)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// ActionVariableDetails prints details of a specific action variable
|
||||
func ActionVariableDetails(variable *gitea.RepoActionVariable) {
|
||||
fmt.Printf("Name: %s\n", variable.Name)
|
||||
fmt.Printf("Value: %s\n", variable.Value)
|
||||
fmt.Printf("Repository ID: %d\n", variable.RepoID)
|
||||
fmt.Printf("Owner ID: %d\n", variable.OwnerID)
|
||||
}
|
||||
|
||||
// ActionVariablesList prints a list of action variables
|
||||
func ActionVariablesList(variables []*gitea.RepoActionVariable, output string) error {
|
||||
t := table{
|
||||
headers: []string{
|
||||
"Name",
|
||||
"Value",
|
||||
"Repository ID",
|
||||
},
|
||||
}
|
||||
|
||||
for _, variable := range variables {
|
||||
// Truncate long values for table display
|
||||
value := variable.Value
|
||||
if len(value) > 50 {
|
||||
value = value[:47] + "..."
|
||||
}
|
||||
|
||||
t.addRow(
|
||||
variable.Name,
|
||||
value,
|
||||
fmt.Sprintf("%d", variable.RepoID),
|
||||
)
|
||||
}
|
||||
|
||||
if len(variables) == 0 {
|
||||
fmt.Printf("No variables found\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
t.sort(0, true)
|
||||
return t.print(output)
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// formatDurationMinutes formats duration in a human-readable way
|
||||
func formatDurationMinutes(started, completed time.Time) string {
|
||||
if started.IsZero() {
|
||||
return ""
|
||||
}
|
||||
|
||||
end := completed
|
||||
if end.IsZero() {
|
||||
end = time.Now()
|
||||
}
|
||||
|
||||
duration := end.Sub(started)
|
||||
if duration < time.Minute {
|
||||
return fmt.Sprintf("%ds", int(duration.Seconds()))
|
||||
}
|
||||
if duration < time.Hour {
|
||||
return fmt.Sprintf("%dm", int(duration.Minutes()))
|
||||
}
|
||||
hours := int(duration.Hours())
|
||||
minutes := int(duration.Minutes()) % 60
|
||||
return fmt.Sprintf("%dh%dm", hours, minutes)
|
||||
}
|
||||
|
||||
// getWorkflowDisplayName returns the display title or falls back to path
|
||||
func getWorkflowDisplayName(run *gitea.ActionWorkflowRun) string {
|
||||
if run.DisplayTitle != "" {
|
||||
return run.DisplayTitle
|
||||
}
|
||||
return run.Path
|
||||
}
|
||||
|
||||
// ActionRunsList prints a list of workflow runs
|
||||
func ActionRunsList(runs []*gitea.ActionWorkflowRun, output string) error {
|
||||
t := table{
|
||||
headers: []string{
|
||||
"ID",
|
||||
"Status",
|
||||
"Workflow",
|
||||
"Branch",
|
||||
"Event",
|
||||
"Started",
|
||||
"Duration",
|
||||
},
|
||||
}
|
||||
|
||||
machineReadable := isMachineReadable(output)
|
||||
|
||||
for _, run := range runs {
|
||||
workflowName := getWorkflowDisplayName(run)
|
||||
duration := formatDurationMinutes(run.StartedAt, run.CompletedAt)
|
||||
|
||||
t.addRow(
|
||||
fmt.Sprintf("%d", run.ID),
|
||||
run.Status,
|
||||
workflowName,
|
||||
run.HeadBranch,
|
||||
run.Event,
|
||||
FormatTime(run.StartedAt, machineReadable),
|
||||
duration,
|
||||
)
|
||||
}
|
||||
|
||||
if len(runs) == 0 {
|
||||
fmt.Printf("No workflow runs found\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
t.sort(0, true)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// ActionRunDetails prints detailed information about a workflow run
|
||||
func ActionRunDetails(run *gitea.ActionWorkflowRun) {
|
||||
workflowName := getWorkflowDisplayName(run)
|
||||
|
||||
fmt.Printf("Run ID: %d\n", run.ID)
|
||||
fmt.Printf("Run Number: %d\n", run.RunNumber)
|
||||
fmt.Printf("Status: %s\n", run.Status)
|
||||
if run.Conclusion != "" {
|
||||
fmt.Printf("Conclusion: %s\n", run.Conclusion)
|
||||
}
|
||||
fmt.Printf("Workflow: %s\n", workflowName)
|
||||
fmt.Printf("Path: %s\n", run.Path)
|
||||
fmt.Printf("Branch: %s\n", run.HeadBranch)
|
||||
fmt.Printf("Event: %s\n", run.Event)
|
||||
fmt.Printf("Head SHA: %s\n", run.HeadSha)
|
||||
fmt.Printf("Started: %s\n", FormatTime(run.StartedAt, false))
|
||||
if !run.CompletedAt.IsZero() {
|
||||
fmt.Printf("Completed: %s\n", FormatTime(run.CompletedAt, false))
|
||||
duration := formatDurationMinutes(run.StartedAt, run.CompletedAt)
|
||||
fmt.Printf("Duration: %s\n", duration)
|
||||
}
|
||||
if run.RunAttempt > 1 {
|
||||
fmt.Printf("Attempt: %d\n", run.RunAttempt)
|
||||
}
|
||||
if run.Actor != nil {
|
||||
fmt.Printf("Triggered by: %s\n", run.Actor.UserName)
|
||||
}
|
||||
if run.HTMLURL != "" {
|
||||
fmt.Printf("URL: %s\n", run.HTMLURL)
|
||||
}
|
||||
}
|
||||
|
||||
// ActionWorkflowJobsList prints a list of workflow jobs
|
||||
func ActionWorkflowJobsList(jobs []*gitea.ActionWorkflowJob, output string) error {
|
||||
t := table{
|
||||
headers: []string{
|
||||
"ID",
|
||||
"Name",
|
||||
"Status",
|
||||
"Runner",
|
||||
"Started",
|
||||
"Duration",
|
||||
},
|
||||
}
|
||||
|
||||
machineReadable := isMachineReadable(output)
|
||||
|
||||
for _, job := range jobs {
|
||||
duration := formatDurationMinutes(job.StartedAt, job.CompletedAt)
|
||||
runner := job.RunnerName
|
||||
if runner == "" {
|
||||
runner = "-"
|
||||
}
|
||||
|
||||
t.addRow(
|
||||
fmt.Sprintf("%d", job.ID),
|
||||
job.Name,
|
||||
job.Status,
|
||||
runner,
|
||||
FormatTime(job.StartedAt, machineReadable),
|
||||
duration,
|
||||
)
|
||||
}
|
||||
|
||||
if len(jobs) == 0 {
|
||||
fmt.Printf("No jobs found\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
t.sort(0, true)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// ActionWorkflowsList prints a list of workflows from the workflow API
|
||||
func ActionWorkflowsList(workflows []*gitea.ActionWorkflow, output string) error {
|
||||
t := table{
|
||||
headers: []string{
|
||||
"ID",
|
||||
"Name",
|
||||
"Path",
|
||||
"State",
|
||||
},
|
||||
}
|
||||
|
||||
for _, wf := range workflows {
|
||||
t.addRow(
|
||||
wf.ID,
|
||||
wf.Name,
|
||||
wf.Path,
|
||||
wf.State,
|
||||
)
|
||||
}
|
||||
|
||||
if len(workflows) == 0 {
|
||||
fmt.Printf("No workflows found\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
t.sort(1, true) // Sort by name column
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// ActionWorkflowDetails prints detailed information about a workflow
|
||||
func ActionWorkflowDetails(wf *gitea.ActionWorkflow) {
|
||||
fmt.Printf("ID: %s\n", wf.ID)
|
||||
fmt.Printf("Name: %s\n", wf.Name)
|
||||
fmt.Printf("Path: %s\n", wf.Path)
|
||||
fmt.Printf("State: %s\n", wf.State)
|
||||
if wf.HTMLURL != "" {
|
||||
fmt.Printf("URL: %s\n", wf.HTMLURL)
|
||||
}
|
||||
if wf.BadgeURL != "" {
|
||||
fmt.Printf("Badge: %s\n", wf.BadgeURL)
|
||||
}
|
||||
if !wf.CreatedAt.IsZero() {
|
||||
fmt.Printf("Created: %s\n", FormatTime(wf.CreatedAt, false))
|
||||
}
|
||||
if !wf.UpdatedAt.IsZero() {
|
||||
fmt.Printf("Updated: %s\n", FormatTime(wf.UpdatedAt, false))
|
||||
}
|
||||
}
|
||||
|
||||
// ActionWorkflowDispatchResult prints the result of a workflow dispatch
|
||||
func ActionWorkflowDispatchResult(details *gitea.RunDetails) {
|
||||
fmt.Printf("Workflow dispatched successfully\n")
|
||||
if details != nil {
|
||||
fmt.Printf("Run ID: %d\n", details.WorkflowRunID)
|
||||
if details.HTMLURL != "" {
|
||||
fmt.Printf("URL: %s\n", details.HTMLURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActionRunsListEmpty(t *testing.T) {
|
||||
// Test with empty runs - should not panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionRunsList panicked with empty list: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
require.NoError(t, ActionRunsList([]*gitea.ActionWorkflowRun{}, ""))
|
||||
}
|
||||
|
||||
func TestActionRunsListWithData(t *testing.T) {
|
||||
runs := []*gitea.ActionWorkflowRun{
|
||||
{
|
||||
ID: 1,
|
||||
Status: "success",
|
||||
DisplayTitle: "Test Workflow",
|
||||
HeadBranch: "main",
|
||||
Event: "push",
|
||||
StartedAt: time.Now().Add(-1 * time.Hour),
|
||||
CompletedAt: time.Now().Add(-30 * time.Minute),
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Status: "in_progress",
|
||||
Path: ".gitea/workflows/test.yml",
|
||||
HeadBranch: "feature",
|
||||
Event: "pull_request",
|
||||
StartedAt: time.Now().Add(-10 * time.Minute),
|
||||
},
|
||||
}
|
||||
|
||||
// Test that it doesn't panic with real data
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionRunsList panicked with data: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
require.NoError(t, ActionRunsList(runs, ""))
|
||||
}
|
||||
|
||||
func TestActionRunDetails(t *testing.T) {
|
||||
run := &gitea.ActionWorkflowRun{
|
||||
ID: 123,
|
||||
RunNumber: 42,
|
||||
Status: "success",
|
||||
Conclusion: "success",
|
||||
DisplayTitle: "Build and Test",
|
||||
Path: ".gitea/workflows/ci.yml",
|
||||
HeadBranch: "main",
|
||||
Event: "push",
|
||||
HeadSha: "abc123def456",
|
||||
StartedAt: time.Now().Add(-2 * time.Hour),
|
||||
CompletedAt: time.Now().Add(-1 * time.Hour),
|
||||
RunAttempt: 1,
|
||||
Actor: &gitea.User{
|
||||
UserName: "testuser",
|
||||
},
|
||||
HTMLURL: "https://gitea.example.com/owner/repo/actions/runs/123",
|
||||
}
|
||||
|
||||
// Test that it doesn't panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionRunDetails panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ActionRunDetails(run)
|
||||
}
|
||||
|
||||
func TestActionWorkflowJobsListEmpty(t *testing.T) {
|
||||
// Test with empty jobs - should not panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionWorkflowJobsList panicked with empty list: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
require.NoError(t, ActionWorkflowJobsList([]*gitea.ActionWorkflowJob{}, ""))
|
||||
}
|
||||
|
||||
func TestActionWorkflowJobsListWithData(t *testing.T) {
|
||||
jobs := []*gitea.ActionWorkflowJob{
|
||||
{
|
||||
ID: 1,
|
||||
Name: "build",
|
||||
Status: "success",
|
||||
RunnerName: "runner-1",
|
||||
StartedAt: time.Now().Add(-30 * time.Minute),
|
||||
CompletedAt: time.Now().Add(-20 * time.Minute),
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Name: "test",
|
||||
Status: "in_progress",
|
||||
RunnerName: "runner-2",
|
||||
StartedAt: time.Now().Add(-5 * time.Minute),
|
||||
},
|
||||
}
|
||||
|
||||
// Test that it doesn't panic with real data
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionWorkflowJobsList panicked with data: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
require.NoError(t, ActionWorkflowJobsList(jobs, ""))
|
||||
}
|
||||
|
||||
func TestActionWorkflowsListEmpty(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionWorkflowsList panicked with empty list: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
require.NoError(t, ActionWorkflowsList([]*gitea.ActionWorkflow{}, ""))
|
||||
}
|
||||
|
||||
func TestActionWorkflowsListWithData(t *testing.T) {
|
||||
workflows := []*gitea.ActionWorkflow{
|
||||
{
|
||||
ID: "1",
|
||||
Name: "CI",
|
||||
Path: ".gitea/workflows/ci.yml",
|
||||
State: "active",
|
||||
},
|
||||
{
|
||||
ID: "2",
|
||||
Name: "Deploy",
|
||||
Path: ".gitea/workflows/deploy.yml",
|
||||
State: "disabled_manually",
|
||||
},
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionWorkflowsList panicked with data: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
require.NoError(t, ActionWorkflowsList(workflows, ""))
|
||||
}
|
||||
|
||||
func TestActionWorkflowDetails(t *testing.T) {
|
||||
wf := &gitea.ActionWorkflow{
|
||||
ID: "1",
|
||||
Name: "CI Pipeline",
|
||||
Path: ".gitea/workflows/ci.yml",
|
||||
State: "active",
|
||||
HTMLURL: "https://gitea.example.com/owner/repo/actions/workflows/ci.yml",
|
||||
BadgeURL: "https://gitea.example.com/owner/repo/actions/workflows/ci.yml/badge.svg",
|
||||
CreatedAt: time.Now().Add(-24 * time.Hour),
|
||||
UpdatedAt: time.Now().Add(-1 * time.Hour),
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionWorkflowDetails panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ActionWorkflowDetails(wf)
|
||||
}
|
||||
|
||||
func TestActionWorkflowDispatchResult(t *testing.T) {
|
||||
details := &gitea.RunDetails{
|
||||
WorkflowRunID: 42,
|
||||
HTMLURL: "https://gitea.example.com/owner/repo/actions/runs/42",
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionWorkflowDispatchResult panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ActionWorkflowDispatchResult(details)
|
||||
}
|
||||
|
||||
func TestActionWorkflowDispatchResultNil(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionWorkflowDispatchResult panicked with nil: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ActionWorkflowDispatchResult(nil)
|
||||
}
|
||||
|
||||
func TestFormatDurationMinutes(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
started time.Time
|
||||
completed time.Time
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "zero started",
|
||||
started: time.Time{},
|
||||
completed: now,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "30 seconds",
|
||||
started: now.Add(-30 * time.Second),
|
||||
completed: now,
|
||||
expected: "30s",
|
||||
},
|
||||
{
|
||||
name: "5 minutes",
|
||||
started: now.Add(-5 * time.Minute),
|
||||
completed: now,
|
||||
expected: "5m",
|
||||
},
|
||||
{
|
||||
name: "in progress (no completed)",
|
||||
started: now.Add(-1 * time.Hour),
|
||||
completed: time.Time{},
|
||||
expected: "1h0m",
|
||||
},
|
||||
{
|
||||
name: "2 hours 30 minutes",
|
||||
started: now.Add(-150 * time.Minute),
|
||||
completed: now,
|
||||
expected: "2h30m",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := formatDurationMinutes(test.started, test.completed)
|
||||
if result != test.expected {
|
||||
t.Errorf("formatDurationMinutes() = %q, want %q", result, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActionSecretsListEmpty(t *testing.T) {
|
||||
// Test with empty secrets - should not panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionSecretsList panicked with empty list: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
require.NoError(t, ActionSecretsList([]*gitea.Secret{}, ""))
|
||||
}
|
||||
|
||||
func TestActionSecretsListWithData(t *testing.T) {
|
||||
secrets := []*gitea.Secret{
|
||||
{
|
||||
Name: "TEST_SECRET_1",
|
||||
Created: time.Now().Add(-24 * time.Hour),
|
||||
},
|
||||
{
|
||||
Name: "TEST_SECRET_2",
|
||||
Created: time.Now().Add(-48 * time.Hour),
|
||||
},
|
||||
}
|
||||
|
||||
// Test that it doesn't panic with real data
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionSecretsList panicked with data: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
require.NoError(t, ActionSecretsList(secrets, ""))
|
||||
|
||||
// Test JSON output format to verify structure
|
||||
var buf bytes.Buffer
|
||||
testTable := table{
|
||||
headers: []string{"Name", "Created"},
|
||||
}
|
||||
|
||||
for _, secret := range secrets {
|
||||
testTable.addRow(secret.Name, FormatTime(secret.Created, true))
|
||||
}
|
||||
|
||||
require.NoError(t, testTable.fprint(&buf, "json"))
|
||||
output := buf.String()
|
||||
|
||||
if !strings.Contains(output, "TEST_SECRET_1") {
|
||||
t.Error("Expected TEST_SECRET_1 in JSON output")
|
||||
}
|
||||
if !strings.Contains(output, "TEST_SECRET_2") {
|
||||
t.Error("Expected TEST_SECRET_2 in JSON output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionVariableDetails(t *testing.T) {
|
||||
variable := &gitea.RepoActionVariable{
|
||||
Name: "TEST_VARIABLE",
|
||||
Value: "test_value",
|
||||
RepoID: 123,
|
||||
OwnerID: 456,
|
||||
}
|
||||
|
||||
// Test that it doesn't panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionVariableDetails panicked: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
ActionVariableDetails(variable)
|
||||
}
|
||||
|
||||
func TestActionVariablesListEmpty(t *testing.T) {
|
||||
// Test with empty variables - should not panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionVariablesList panicked with empty list: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
require.NoError(t, ActionVariablesList([]*gitea.RepoActionVariable{}, ""))
|
||||
}
|
||||
|
||||
func TestActionVariablesListWithData(t *testing.T) {
|
||||
variables := []*gitea.RepoActionVariable{
|
||||
{
|
||||
Name: "TEST_VARIABLE_1",
|
||||
Value: "short_value",
|
||||
RepoID: 123,
|
||||
OwnerID: 456,
|
||||
},
|
||||
{
|
||||
Name: "TEST_VARIABLE_2",
|
||||
Value: strings.Repeat("a", 60), // Long value to test truncation
|
||||
RepoID: 124,
|
||||
OwnerID: 457,
|
||||
},
|
||||
}
|
||||
|
||||
// Test that it doesn't panic with real data
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionVariablesList panicked with data: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
require.NoError(t, ActionVariablesList(variables, ""))
|
||||
|
||||
// Test JSON output format to verify structure and truncation
|
||||
var buf bytes.Buffer
|
||||
testTable := table{
|
||||
headers: []string{"Name", "Value", "Repository ID"},
|
||||
}
|
||||
|
||||
for _, variable := range variables {
|
||||
value := variable.Value
|
||||
if len(value) > 50 {
|
||||
value = value[:47] + "..."
|
||||
}
|
||||
testTable.addRow(variable.Name, value, strconv.Itoa(int(variable.RepoID)))
|
||||
}
|
||||
|
||||
require.NoError(t, testTable.fprint(&buf, "json"))
|
||||
output := buf.String()
|
||||
|
||||
if !strings.Contains(output, "TEST_VARIABLE_1") {
|
||||
t.Error("Expected TEST_VARIABLE_1 in JSON output")
|
||||
}
|
||||
if !strings.Contains(output, "TEST_VARIABLE_2") {
|
||||
t.Error("Expected TEST_VARIABLE_2 in JSON output")
|
||||
}
|
||||
|
||||
// Check that long value is truncated in our test table
|
||||
if strings.Contains(output, strings.Repeat("a", 60)) {
|
||||
t.Error("Long value should be truncated in table output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionVariablesListValueTruncation(t *testing.T) {
|
||||
variable := &gitea.RepoActionVariable{
|
||||
Name: "LONG_VALUE_VARIABLE",
|
||||
Value: strings.Repeat("abcdefghij", 10), // 100 characters
|
||||
RepoID: 123,
|
||||
OwnerID: 456,
|
||||
}
|
||||
|
||||
// Test that it doesn't panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("ActionVariablesList panicked with long value: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
require.NoError(t, ActionVariablesList([]*gitea.RepoActionVariable{variable}, ""))
|
||||
|
||||
// Test the truncation logic directly
|
||||
value := variable.Value
|
||||
if len(value) > 50 {
|
||||
value = value[:47] + "..."
|
||||
}
|
||||
|
||||
if len(value) != 50 { // 47 chars + "..." = 50
|
||||
t.Errorf("Truncated value should be 50 characters, got %d", len(value))
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(value, "...") {
|
||||
t.Error("Truncated value should end with '...'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTableSorting(t *testing.T) {
|
||||
// Test that the table sorting works correctly
|
||||
secrets := []*gitea.Secret{
|
||||
{Name: "Z_SECRET", Created: time.Now()},
|
||||
{Name: "A_SECRET", Created: time.Now()},
|
||||
{Name: "M_SECRET", Created: time.Now()},
|
||||
}
|
||||
|
||||
// Test the table sorting logic
|
||||
table := table{
|
||||
headers: []string{"Name", "Created"},
|
||||
}
|
||||
|
||||
for _, secret := range secrets {
|
||||
table.addRow(secret.Name, FormatTime(secret.Created, true))
|
||||
}
|
||||
|
||||
// Sort by first column (Name) in ascending order (false = ascending)
|
||||
table.sort(0, false)
|
||||
|
||||
// Check that the first row is A_SECRET after ascending sorting
|
||||
if table.values[0][0] != "A_SECRET" {
|
||||
t.Errorf("Expected first sorted value to be 'A_SECRET', got '%s'", table.values[0][0])
|
||||
}
|
||||
|
||||
// Check that the last row is Z_SECRET after ascending sorting
|
||||
if table.values[2][0] != "Z_SECRET" {
|
||||
t.Errorf("Expected last sorted value to be 'Z_SECRET', got '%s'", table.values[2][0])
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
func formatByteSize(size int64) string {
|
||||
if size < 1024 {
|
||||
return fmt.Sprintf("%d B", size)
|
||||
}
|
||||
return formatSize(size / 1024)
|
||||
}
|
||||
|
||||
// ReleaseAttachmentsList prints a listing of release attachments
|
||||
func ReleaseAttachmentsList(attachments []*gitea.Attachment, output string) error {
|
||||
t := tableWithHeader(
|
||||
"Name",
|
||||
"Size",
|
||||
)
|
||||
|
||||
for _, attachment := range attachments {
|
||||
t.addRow(
|
||||
attachment.Name,
|
||||
formatByteSize(attachment.Size),
|
||||
)
|
||||
}
|
||||
|
||||
return t.print(output)
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// BranchesList prints a listing of the branches
|
||||
func BranchesList(branches []*gitea.Branch, protections []*gitea.BranchProtection, output string, fields []string) error {
|
||||
printables := make([]printable, len(branches))
|
||||
|
||||
for i, branch := range branches {
|
||||
var protection *gitea.BranchProtection
|
||||
for _, p := range protections {
|
||||
if p.BranchName == branch.Name {
|
||||
protection = p
|
||||
}
|
||||
}
|
||||
printables[i] = &printableBranch{branch, protection}
|
||||
}
|
||||
|
||||
t := tableFromItems(fields, printables, isMachineReadable(output))
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
type printableBranch struct {
|
||||
branch *gitea.Branch
|
||||
protection *gitea.BranchProtection
|
||||
}
|
||||
|
||||
func (x printableBranch) FormatField(field string, machineReadable bool) string {
|
||||
switch field {
|
||||
case "name":
|
||||
return x.branch.Name
|
||||
case "protected":
|
||||
return fmt.Sprintf("%t", x.branch.Protected)
|
||||
case "user-can-merge":
|
||||
return fmt.Sprintf("%t", x.branch.UserCanMerge)
|
||||
case "user-can-push":
|
||||
return fmt.Sprintf("%t", x.branch.UserCanPush)
|
||||
case "protection":
|
||||
if x.protection != nil {
|
||||
approving := ""
|
||||
for _, entry := range x.protection.ApprovalsWhitelistTeams {
|
||||
approving += entry + "/"
|
||||
}
|
||||
for _, entry := range x.protection.ApprovalsWhitelistUsernames {
|
||||
approving += entry + "/"
|
||||
}
|
||||
merging := ""
|
||||
for _, entry := range x.protection.MergeWhitelistTeams {
|
||||
merging += entry + "/"
|
||||
}
|
||||
for _, entry := range x.protection.MergeWhitelistUsernames {
|
||||
merging += entry + "/"
|
||||
}
|
||||
pushing := ""
|
||||
for _, entry := range x.protection.PushWhitelistTeams {
|
||||
pushing += entry + "/"
|
||||
}
|
||||
for _, entry := range x.protection.PushWhitelistUsernames {
|
||||
pushing += entry + "/"
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"- enable-push: %t\n- approving: %s\n- merging: %s\n- pushing: %s\n",
|
||||
x.protection.EnablePush, approving, merging, pushing,
|
||||
)
|
||||
}
|
||||
return "<None>"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// BranchFields are all available fields to print with BranchesList()
|
||||
var BranchFields = []string{
|
||||
"name",
|
||||
"protected",
|
||||
"user-can-merge",
|
||||
"user-can-push",
|
||||
"protection",
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
"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/")
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// Comments renders a list of comments to stdout
|
||||
func Comments(comments []*gitea.Comment) {
|
||||
var baseURL string
|
||||
if len(comments) != 0 {
|
||||
baseURL = getRepoURL(comments[0].HTMLURL)
|
||||
}
|
||||
|
||||
out := make([]string, len(comments))
|
||||
for i, c := range comments {
|
||||
out[i] = formatComment(c)
|
||||
}
|
||||
|
||||
_ = outputMarkdown(fmt.Sprintf(
|
||||
// this will become a heading by means of the first --- from a comment
|
||||
"Comments\n%s",
|
||||
strings.Join(out, "\n"),
|
||||
), baseURL)
|
||||
}
|
||||
|
||||
// CommentsList prints comments in tabular form, including IDs so they can be
|
||||
// passed to 'tea comments edit' / 'tea comments delete'.
|
||||
func CommentsList(comments []*gitea.Comment, output string) error {
|
||||
if len(comments) == 0 {
|
||||
fmt.Println("No comments found")
|
||||
return nil
|
||||
}
|
||||
|
||||
t := tableWithHeader(
|
||||
"ID",
|
||||
"Author",
|
||||
"Created",
|
||||
"Updated",
|
||||
"Body",
|
||||
)
|
||||
|
||||
for _, c := range comments {
|
||||
updated := ""
|
||||
if c.Updated.After(c.Created) {
|
||||
updated = FormatTime(c.Updated, false)
|
||||
}
|
||||
t.addRow(
|
||||
fmt.Sprintf("%d", c.ID),
|
||||
"@"+c.Poster.UserName,
|
||||
FormatTime(c.Created, false),
|
||||
updated,
|
||||
summarizeBody(c.Body),
|
||||
)
|
||||
}
|
||||
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
func summarizeBody(body string) string {
|
||||
const max = 80
|
||||
var b strings.Builder
|
||||
for _, r := range body {
|
||||
if r == '\n' || r == '\r' {
|
||||
b.WriteByte(' ')
|
||||
} else {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
s := b.String()
|
||||
if len(s) > max {
|
||||
return s[:max-1] + "…"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Comment renders a comment to stdout
|
||||
func Comment(c *gitea.Comment) {
|
||||
_ = outputMarkdown(formatComment(c), getRepoURL(c.HTMLURL))
|
||||
}
|
||||
|
||||
func formatComment(c *gitea.Comment) string {
|
||||
edited := ""
|
||||
if c.Updated.After(c.Created) {
|
||||
edited = fmt.Sprintf(" *(edited on %s)*", FormatTime(c.Updated, false))
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"---\n\n**@%s** wrote on %s%s:\n\n%s\n",
|
||||
c.Poster.UserName,
|
||||
FormatTime(c.Created, false),
|
||||
edited,
|
||||
c.Body,
|
||||
)
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
"github.com/muesli/termenv"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// IsInteractive checks if the output is piped, but NOT if the session is run interactively..
|
||||
func IsInteractive() bool {
|
||||
return term.IsTerminal(int(os.Stdout.Fd()))
|
||||
}
|
||||
|
||||
// captures the repo URL part <host>/<owner>/<repo> of an url
|
||||
var repoURLRegex = regexp.MustCompile("^([[:alnum:]]+://[^/]+(?:/[[:alnum:]]+){2})/.*")
|
||||
|
||||
func getRepoURL(resourceURL string) string {
|
||||
return repoURLRegex.ReplaceAllString(resourceURL, "$1/")
|
||||
}
|
||||
|
||||
// formatSize get kb in int and return string
|
||||
func formatSize(kb int64) string {
|
||||
if kb < 1024 {
|
||||
return fmt.Sprintf("%d KB", kb)
|
||||
}
|
||||
mb := kb / 1024
|
||||
if mb < 1024 {
|
||||
return fmt.Sprintf("%d MB", mb)
|
||||
}
|
||||
gb := mb / 1024
|
||||
if gb < 1024 {
|
||||
return fmt.Sprintf("%d GB", gb)
|
||||
}
|
||||
return fmt.Sprintf("%d TB", gb/1024)
|
||||
}
|
||||
|
||||
// FormatTime provides a string for the given time value.
|
||||
// If machineReadable is set, a UTC RFC3339 string is returned,
|
||||
// otherwise a simplified string in local time is used.
|
||||
func FormatTime(t time.Time, machineReadable bool) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
|
||||
if machineReadable {
|
||||
return t.UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
location, err := time.LoadLocation("Local")
|
||||
if err != nil {
|
||||
return t.Format("2006-01-02 15:04 UTC")
|
||||
}
|
||||
return t.In(location).Format("2006-01-02 15:04")
|
||||
}
|
||||
|
||||
func formatDuration(seconds int64, outputType string) string {
|
||||
if isMachineReadable(outputType) {
|
||||
return fmt.Sprint(seconds)
|
||||
}
|
||||
return time.Duration(1e9 * seconds).String()
|
||||
}
|
||||
|
||||
func formatLabel(label *gitea.Label, allowColor bool, text string) string {
|
||||
colorProfile := termenv.Ascii
|
||||
if allowColor {
|
||||
colorProfile = termenv.EnvColorProfile()
|
||||
}
|
||||
if len(text) == 0 {
|
||||
text = label.Name
|
||||
}
|
||||
styled := termenv.String(text)
|
||||
styled = styled.Foreground(colorProfile.Color("#" + label.Color))
|
||||
return fmt.Sprint(styled)
|
||||
}
|
||||
|
||||
func formatPermission(p *gitea.Permission) string {
|
||||
if p.Admin {
|
||||
return "admin"
|
||||
} else if p.Push {
|
||||
return "write"
|
||||
}
|
||||
return "read"
|
||||
}
|
||||
|
||||
func formatUserName(u *gitea.User) string {
|
||||
if len(u.FullName) == 0 {
|
||||
return u.UserName
|
||||
}
|
||||
return u.FullName
|
||||
}
|
||||
|
||||
func formatBoolean(b bool, allowIcons bool) string {
|
||||
if !allowIcons {
|
||||
return fmt.Sprintf("%v", b)
|
||||
}
|
||||
|
||||
styled := "✔"
|
||||
if !b {
|
||||
styled = "✖"
|
||||
}
|
||||
|
||||
return styled
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
"github.com/enescakir/emoji"
|
||||
)
|
||||
|
||||
// IssueDetails print an issue rendered to stdout
|
||||
func IssueDetails(issue *gitea.Issue, reactions []*gitea.Reaction) {
|
||||
out := fmt.Sprintf(
|
||||
"# #%d %s (%s)\n@%s created %s\n\n%s\n",
|
||||
issue.Index,
|
||||
issue.Title,
|
||||
issue.State,
|
||||
issue.Poster.UserName,
|
||||
FormatTime(issue.Created, false),
|
||||
issue.Body,
|
||||
)
|
||||
|
||||
if len(reactions) > 0 {
|
||||
out += fmt.Sprintf("\n---\n\n%s\n", formatReactions(reactions))
|
||||
}
|
||||
|
||||
_ = outputMarkdown(out, getRepoURL(issue.HTMLURL))
|
||||
}
|
||||
|
||||
func formatReactions(reactions []*gitea.Reaction) string {
|
||||
reactionCounts := make(map[string]uint16)
|
||||
for _, r := range reactions {
|
||||
reactionCounts[r.Reaction]++
|
||||
}
|
||||
|
||||
reactionStrings := make([]string, 0, len(reactionCounts))
|
||||
for reaction, count := range reactionCounts {
|
||||
reactionStrings = append(reactionStrings, fmt.Sprintf("%dx :%s:", count, reaction))
|
||||
}
|
||||
|
||||
return emoji.Parse(strings.Join(reactionStrings, " | "))
|
||||
}
|
||||
|
||||
// IssuesPullsList prints a listing of issues & pulls
|
||||
func IssuesPullsList(issues []*gitea.Issue, output string, fields []string) error {
|
||||
return printIssues(issues, output, fields)
|
||||
}
|
||||
|
||||
// IssueFields are all available fields to print with IssuesList()
|
||||
var IssueFields = []string{
|
||||
"index",
|
||||
"state",
|
||||
"kind",
|
||||
"author",
|
||||
"author-id",
|
||||
"url",
|
||||
|
||||
"title",
|
||||
"body",
|
||||
|
||||
"created",
|
||||
"updated",
|
||||
"deadline",
|
||||
|
||||
"assignees",
|
||||
"milestone",
|
||||
"labels",
|
||||
"comments",
|
||||
"owner",
|
||||
"repo",
|
||||
}
|
||||
|
||||
func printIssues(issues []*gitea.Issue, output string, fields []string) error {
|
||||
labelMap := map[int64]string{}
|
||||
printables := make([]printable, len(issues))
|
||||
machineReadable := isMachineReadable(output)
|
||||
|
||||
for i, x := range issues {
|
||||
// pre-serialize labels for performance
|
||||
for _, label := range x.Labels {
|
||||
if _, ok := labelMap[label.ID]; !ok {
|
||||
labelMap[label.ID] = formatLabel(label, !machineReadable, "")
|
||||
}
|
||||
}
|
||||
// store items with printable interface
|
||||
printables[i] = &printableIssue{x, &labelMap}
|
||||
}
|
||||
|
||||
t := tableFromItems(fields, printables, machineReadable)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
type printableIssue struct {
|
||||
*gitea.Issue
|
||||
formattedLabels *map[int64]string
|
||||
}
|
||||
|
||||
func (x printableIssue) FormatField(field string, machineReadable bool) string {
|
||||
switch field {
|
||||
case "index":
|
||||
return fmt.Sprintf("%d", x.Index)
|
||||
case "state":
|
||||
return string(x.State)
|
||||
case "kind":
|
||||
if x.PullRequest != nil {
|
||||
return "Pull"
|
||||
}
|
||||
return "Issue"
|
||||
case "author":
|
||||
return formatUserName(x.Poster)
|
||||
case "author-id":
|
||||
return x.Poster.UserName
|
||||
case "url":
|
||||
return x.HTMLURL
|
||||
case "title":
|
||||
return x.Title
|
||||
case "body":
|
||||
return x.Body
|
||||
case "created":
|
||||
return FormatTime(x.Created, machineReadable)
|
||||
case "updated":
|
||||
return FormatTime(x.Updated, machineReadable)
|
||||
case "deadline":
|
||||
if x.Deadline == nil {
|
||||
return ""
|
||||
}
|
||||
return FormatTime(*x.Deadline, machineReadable)
|
||||
case "milestone":
|
||||
if x.Milestone != nil {
|
||||
return x.Milestone.Title
|
||||
}
|
||||
return ""
|
||||
case "labels":
|
||||
labels := make([]string, len(x.Labels))
|
||||
for i, l := range x.Labels {
|
||||
labels[i] = (*x.formattedLabels)[l.ID]
|
||||
}
|
||||
return strings.Join(labels, " ")
|
||||
case "assignees":
|
||||
assignees := make([]string, len(x.Assignees))
|
||||
for i, a := range x.Assignees {
|
||||
assignees[i] = formatUserName(a)
|
||||
}
|
||||
return strings.Join(assignees, " ")
|
||||
case "comments":
|
||||
return fmt.Sprintf("%d", x.Comments)
|
||||
case "owner":
|
||||
return x.Repository.Owner
|
||||
case "repo":
|
||||
return x.Repository.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
gitea "gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// LabelsList prints a listing of labels
|
||||
func LabelsList(labels []*gitea.Label, output string) error {
|
||||
t := tableWithHeader(
|
||||
"Index",
|
||||
"Color",
|
||||
"Name",
|
||||
"Description",
|
||||
"Level",
|
||||
)
|
||||
|
||||
for _, label := range labels {
|
||||
level := "Repository"
|
||||
if strings.Contains(label.URL, "/orgs/") {
|
||||
level = "Organization"
|
||||
}
|
||||
t.addRow(
|
||||
strconv.FormatInt(label.ID, 10),
|
||||
formatLabel(label, !isMachineReadable(output), label.Color),
|
||||
label.Name,
|
||||
label.Description,
|
||||
level,
|
||||
)
|
||||
}
|
||||
return t.print(output)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dev/tea/modules/config"
|
||||
)
|
||||
|
||||
// LoginDetails print login entry to stdout
|
||||
func LoginDetails(login *config.Login) {
|
||||
in := fmt.Sprintf("# %s\n\n[@%s](%s/%s)\n",
|
||||
login.Name,
|
||||
login.User,
|
||||
strings.TrimSuffix(login.URL, "/"),
|
||||
login.User,
|
||||
)
|
||||
if len(login.SSHKey) != 0 {
|
||||
in += fmt.Sprintf("\nSSH Key: '%s' via %s\n",
|
||||
login.SSHKey,
|
||||
login.SSHHost,
|
||||
)
|
||||
}
|
||||
in += fmt.Sprintf("\nCreated: %s", time.Unix(login.Created, 0).Format(time.RFC822))
|
||||
|
||||
_ = outputMarkdown(in, "")
|
||||
}
|
||||
|
||||
// LoginsList prints a listing of logins
|
||||
func LoginsList(logins []config.Login, output string) error {
|
||||
t := tableWithHeader(
|
||||
"Name",
|
||||
"URL",
|
||||
"SSHHost",
|
||||
"User",
|
||||
"Default",
|
||||
)
|
||||
|
||||
for _, l := range logins {
|
||||
t.addRow(
|
||||
l.Name,
|
||||
l.URL,
|
||||
l.GetSSHHost(),
|
||||
l.User,
|
||||
fmt.Sprint(l.Default),
|
||||
)
|
||||
}
|
||||
|
||||
return t.print(output)
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LoginStatus contains the authentication status of a single configured login.
|
||||
type LoginStatus struct {
|
||||
Name string
|
||||
URL string
|
||||
User string
|
||||
Valid bool
|
||||
AuthMethod string
|
||||
TokenExpiry time.Time
|
||||
Helper bool
|
||||
Default bool
|
||||
Error string
|
||||
}
|
||||
|
||||
// LoginStatusFields are the available fields to print with LoginStatuses.
|
||||
var LoginStatusFields = []string{
|
||||
"name",
|
||||
"url",
|
||||
"user",
|
||||
"valid",
|
||||
"auth_method",
|
||||
"token_expiry",
|
||||
"helper",
|
||||
"default",
|
||||
}
|
||||
|
||||
// LoginStatuses prints authentication status for one or more logins.
|
||||
func LoginStatuses(statuses []LoginStatus, output string) error {
|
||||
if output != "" {
|
||||
printables := make([]printable, len(statuses))
|
||||
for i := range statuses {
|
||||
printables[i] = statuses[i]
|
||||
}
|
||||
t := tableFromItems(LoginStatusFields, printables, isMachineReadable(output))
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
if len(statuses) == 0 {
|
||||
fmt.Println("No logins configured.")
|
||||
return nil
|
||||
}
|
||||
|
||||
for i, status := range statuses {
|
||||
if i > 0 {
|
||||
fmt.Println()
|
||||
}
|
||||
printLoginStatusReport(status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printLoginStatusReport(status LoginStatus) {
|
||||
name := status.Name
|
||||
if status.Default {
|
||||
name += " (default)"
|
||||
}
|
||||
fmt.Println(name)
|
||||
|
||||
if status.Valid {
|
||||
line := " ✔ Logged in to " + status.URL
|
||||
if status.User != "" {
|
||||
line += " as " + status.User
|
||||
}
|
||||
fmt.Println(line)
|
||||
|
||||
tokenLine := " ✔ Token is valid"
|
||||
if status.AuthMethod != "" {
|
||||
tokenLine += " (" + status.AuthMethod
|
||||
if !status.TokenExpiry.IsZero() {
|
||||
tokenLine += ", " + formatTokenExpiry(status.TokenExpiry)
|
||||
}
|
||||
tokenLine += ")"
|
||||
}
|
||||
fmt.Println(tokenLine)
|
||||
} else {
|
||||
message := status.Error
|
||||
if message == "" {
|
||||
message = "Login failed"
|
||||
}
|
||||
fmt.Println(" ✗ " + message)
|
||||
}
|
||||
|
||||
if status.Helper {
|
||||
fmt.Println(" ✔ Git credential helper configured")
|
||||
} else {
|
||||
fmt.Println(" ✗ Git credential helper not configured")
|
||||
}
|
||||
}
|
||||
|
||||
func formatExpiryDuration(t time.Time) string {
|
||||
d := time.Until(t)
|
||||
if d < 0 {
|
||||
return "expired"
|
||||
}
|
||||
if d < time.Minute {
|
||||
return "in less than a minute"
|
||||
}
|
||||
return "in " + strings.TrimSuffix(d.Truncate(time.Minute).String(), "0s")
|
||||
}
|
||||
|
||||
func formatTokenExpiry(t time.Time) string {
|
||||
if t.Before(time.Now()) {
|
||||
return "expired"
|
||||
}
|
||||
return "expires " + formatExpiryDuration(t)
|
||||
}
|
||||
|
||||
// FormatField implements the printable interface for LoginStatus.
|
||||
func (s LoginStatus) FormatField(field string, machineReadable bool) string {
|
||||
switch field {
|
||||
case "name":
|
||||
return s.Name
|
||||
case "url":
|
||||
return s.URL
|
||||
case "user":
|
||||
return s.User
|
||||
case "valid":
|
||||
return formatBoolean(s.Valid, !machineReadable)
|
||||
case "auth_method":
|
||||
return s.AuthMethod
|
||||
case "token_expiry":
|
||||
if s.TokenExpiry.IsZero() {
|
||||
return ""
|
||||
}
|
||||
if machineReadable {
|
||||
return FormatTime(s.TokenExpiry, true)
|
||||
}
|
||||
return formatExpiryDuration(s.TokenExpiry)
|
||||
case "helper":
|
||||
return formatBoolean(s.Helper, !machineReadable)
|
||||
case "default":
|
||||
return formatBoolean(s.Default, !machineReadable)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoginStatusFormatField(t *testing.T) {
|
||||
status := LoginStatus{
|
||||
Name: "gitea",
|
||||
URL: "https://gitea.com",
|
||||
User: "alice",
|
||||
Valid: true,
|
||||
AuthMethod: "oauth",
|
||||
TokenExpiry: time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC),
|
||||
Helper: true,
|
||||
Default: true,
|
||||
}
|
||||
|
||||
assert.Equal(t, "gitea", status.FormatField("name", false))
|
||||
assert.Equal(t, "https://gitea.com", status.FormatField("url", false))
|
||||
assert.Equal(t, "alice", status.FormatField("user", false))
|
||||
assert.Equal(t, "true", status.FormatField("valid", true))
|
||||
assert.Equal(t, "✔", status.FormatField("valid", false))
|
||||
assert.Equal(t, "oauth", status.FormatField("auth_method", true))
|
||||
assert.Equal(t, "2026-08-27T12:00:00Z", status.FormatField("token_expiry", true))
|
||||
assert.Equal(t, "true", status.FormatField("helper", true))
|
||||
assert.Equal(t, "✔", status.FormatField("default", false))
|
||||
}
|
||||
|
||||
func TestFormatExpiryDurationExpired(t *testing.T) {
|
||||
assert.Equal(t, "expired", formatExpiryDuration(time.Now().Add(-time.Hour)))
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"charm.land/glamour/v2"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// outputMarkdown prints markdown to stdout, formatted for terminals.
|
||||
// If the input could not be parsed, it is printed unformatted, the error
|
||||
// is returned anyway.
|
||||
func outputMarkdown(markdown string, baseURL string) error {
|
||||
var renderer *glamour.TermRenderer
|
||||
var err error
|
||||
if IsInteractive() {
|
||||
renderer, err = glamour.NewTermRenderer(
|
||||
glamour.WithBaseURL(baseURL),
|
||||
glamour.WithPreservedNewLines(),
|
||||
glamour.WithWordWrap(getWordWrap()),
|
||||
)
|
||||
} else {
|
||||
renderer, err = glamour.NewTermRenderer(
|
||||
glamour.WithStandardStyle("notty"),
|
||||
glamour.WithBaseURL(baseURL),
|
||||
glamour.WithPreservedNewLines(),
|
||||
glamour.WithWordWrap(getWordWrap()),
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Print(markdown)
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := renderer.Render(markdown)
|
||||
if err != nil {
|
||||
fmt.Print(markdown)
|
||||
return err
|
||||
}
|
||||
fmt.Print(out)
|
||||
return nil
|
||||
}
|
||||
|
||||
// stolen from https://github.com/charmbracelet/glow/blob/e9d728c/main.go#L152-L165
|
||||
func getWordWrap() int {
|
||||
fd := int(os.Stdout.Fd())
|
||||
width := 80
|
||||
if term.IsTerminal(fd) {
|
||||
if w, _, err := term.GetSize(fd); err == nil {
|
||||
width = w
|
||||
}
|
||||
}
|
||||
if width > 120 {
|
||||
width = 120
|
||||
}
|
||||
return width
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// MilestoneDetails print an milestone formatted to stdout
|
||||
func MilestoneDetails(milestone *gitea.Milestone) {
|
||||
fmt.Printf("%s\n",
|
||||
milestone.Title,
|
||||
)
|
||||
if len(milestone.Description) != 0 {
|
||||
outputMarkdown(milestone.Description, "")
|
||||
}
|
||||
if milestone.Deadline != nil && !milestone.Deadline.IsZero() {
|
||||
fmt.Printf("\nDeadline: %s\n", FormatTime(*milestone.Deadline, false))
|
||||
}
|
||||
}
|
||||
|
||||
// MilestonesList prints a listing of milestones
|
||||
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)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// MilestoneFields are all available fields to print with MilestonesList
|
||||
var MilestoneFields = []string{
|
||||
"title",
|
||||
"state",
|
||||
"items_open",
|
||||
"items_closed",
|
||||
"items",
|
||||
"duedate",
|
||||
"description",
|
||||
"created",
|
||||
"updated",
|
||||
"closed",
|
||||
"id",
|
||||
}
|
||||
|
||||
type printableMilestone struct {
|
||||
*gitea.Milestone
|
||||
}
|
||||
|
||||
func (m printableMilestone) FormatField(field string, machineReadable bool) string {
|
||||
switch field {
|
||||
case "title":
|
||||
return m.Title
|
||||
case "state":
|
||||
return string(m.State)
|
||||
case "items_open":
|
||||
return fmt.Sprintf("%d", m.OpenIssues)
|
||||
case "items_closed":
|
||||
return fmt.Sprintf("%d", m.ClosedIssues)
|
||||
case "items":
|
||||
return fmt.Sprintf("%d/%d", m.OpenIssues, m.ClosedIssues)
|
||||
case "duedate":
|
||||
if m.Deadline != nil && !m.Deadline.IsZero() {
|
||||
return FormatTime(*m.Deadline, machineReadable)
|
||||
}
|
||||
case "id":
|
||||
return fmt.Sprintf("%d", m.ID)
|
||||
case "description":
|
||||
return m.Description
|
||||
case "created":
|
||||
return FormatTime(m.Created, machineReadable)
|
||||
case "updated":
|
||||
if m.Updated != nil {
|
||||
return FormatTime(*m.Updated, machineReadable)
|
||||
}
|
||||
case "closed":
|
||||
if m.Closed != nil {
|
||||
return FormatTime(*m.Closed, machineReadable)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// NotificationsList prints a listing of notification threads
|
||||
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))
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// NotificationFields are all available fields to print with NotificationsList
|
||||
var NotificationFields = []string{
|
||||
"id",
|
||||
"status",
|
||||
"updated",
|
||||
|
||||
// these are about the notification subject
|
||||
"index",
|
||||
"type",
|
||||
"state",
|
||||
"title",
|
||||
"repository",
|
||||
}
|
||||
|
||||
type printableNotification struct {
|
||||
*gitea.NotificationThread
|
||||
}
|
||||
|
||||
func (n printableNotification) FormatField(field string, machineReadable bool) string {
|
||||
switch field {
|
||||
case "id":
|
||||
return fmt.Sprintf("%d", n.ID)
|
||||
|
||||
case "status":
|
||||
status := "read"
|
||||
if n.Pinned {
|
||||
status = "pinned"
|
||||
} else if n.Unread {
|
||||
status = "unread"
|
||||
}
|
||||
return status
|
||||
|
||||
case "updated":
|
||||
return FormatTime(n.UpdatedAt, machineReadable)
|
||||
|
||||
case "index":
|
||||
var index string
|
||||
if n.Subject.Type == "Issue" || n.Subject.Type == "Pull" {
|
||||
index = n.Subject.URL
|
||||
urlParts := strings.Split(n.Subject.URL, "/")
|
||||
if len(urlParts) != 0 {
|
||||
index = urlParts[len(urlParts)-1]
|
||||
}
|
||||
}
|
||||
return index
|
||||
|
||||
case "type":
|
||||
return string(n.Subject.Type)
|
||||
|
||||
case "state":
|
||||
return string(n.Subject.State)
|
||||
|
||||
case "title":
|
||||
return n.Subject.Title
|
||||
|
||||
case "repo", "repository":
|
||||
return n.Repository.FullName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// OrganizationDetails prints details of an org with formatting
|
||||
func OrganizationDetails(org *gitea.Organization) {
|
||||
_ = outputMarkdown(fmt.Sprintf(
|
||||
"# %s\n%s\n\n- Visibility: %s\n- Location: %s\n- Website: %s\n",
|
||||
org.UserName,
|
||||
org.Description,
|
||||
org.Visibility,
|
||||
org.Location,
|
||||
org.Website,
|
||||
), "")
|
||||
}
|
||||
|
||||
// OrganizationsList prints a listing of the organizations
|
||||
func OrganizationsList(organizations []*gitea.Organization, output string) error {
|
||||
if len(organizations) == 0 {
|
||||
fmt.Println("No organizations found")
|
||||
return nil
|
||||
}
|
||||
|
||||
t := tableWithHeader(
|
||||
"Name",
|
||||
"FullName",
|
||||
"Website",
|
||||
"Location",
|
||||
"Description",
|
||||
)
|
||||
|
||||
for _, org := range organizations {
|
||||
t.addRow(
|
||||
org.UserName,
|
||||
org.FullName,
|
||||
org.Website,
|
||||
org.Location,
|
||||
org.Description,
|
||||
)
|
||||
}
|
||||
|
||||
return t.print(output)
|
||||
}
|
||||
@@ -1,290 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
var ciStatusSymbols = map[gitea.StatusState]string{
|
||||
gitea.StatusSuccess: "✓ ",
|
||||
gitea.StatusPending: "⏳ ",
|
||||
gitea.StatusWarning: "⚠ ",
|
||||
gitea.StatusError: "✘ ",
|
||||
gitea.StatusFailure: "❌ ",
|
||||
}
|
||||
|
||||
// PullDetails print an pull rendered to stdout
|
||||
func PullDetails(pr *gitea.PullRequest, reviews []*gitea.PullReview, ciStatus *gitea.CombinedStatus) {
|
||||
base := pr.Base.Name
|
||||
head := formatPRHead(pr)
|
||||
state := formatPRState(pr)
|
||||
|
||||
out := fmt.Sprintf(
|
||||
"# #%d %s (%s)\n@%s created %s\t**%s** <- **%s**\n\n%s\n\n",
|
||||
pr.Index,
|
||||
pr.Title,
|
||||
state,
|
||||
pr.Poster.UserName,
|
||||
FormatTime(*pr.Created, false),
|
||||
base,
|
||||
head,
|
||||
pr.Body,
|
||||
)
|
||||
|
||||
if ciStatus != nil || len(reviews) != 0 || pr.State == gitea.StateOpen {
|
||||
out += "---\n"
|
||||
}
|
||||
|
||||
out += formatReviews(pr, reviews)
|
||||
|
||||
if ciStatus != nil && len(ciStatus.Statuses) != 0 {
|
||||
out += "- CI:\n"
|
||||
for _, s := range ciStatus.Statuses {
|
||||
symbol := ciStatusSymbols[s.State]
|
||||
if s.TargetURL != "" {
|
||||
out += fmt.Sprintf(" - %s[**%s**](%s)", symbol, s.Context, s.TargetURL)
|
||||
} else {
|
||||
out += fmt.Sprintf(" - %s**%s**", symbol, s.Context)
|
||||
}
|
||||
if s.Description != "" {
|
||||
out += fmt.Sprintf(": %s", s.Description)
|
||||
}
|
||||
out += "\n"
|
||||
}
|
||||
}
|
||||
|
||||
if pr.State == gitea.StateOpen {
|
||||
switch {
|
||||
case pr.Mergeable:
|
||||
out += "- No Conflicts\n"
|
||||
case pr.Draft:
|
||||
out += "- Draft (not mergeable until marked ready)\n"
|
||||
default:
|
||||
out += "- **Conflicting files**\n"
|
||||
}
|
||||
}
|
||||
|
||||
if pr.AllowMaintainerEdit {
|
||||
out += "- Maintainers are allowed to edit\n"
|
||||
}
|
||||
|
||||
if pr.HTMLURL != "" {
|
||||
out = strings.TrimRight(out, "\n") + "\n\n" + pr.HTMLURL + "\n"
|
||||
}
|
||||
|
||||
outputMarkdown(out, getRepoURL(pr.HTMLURL))
|
||||
}
|
||||
|
||||
func formatPRHead(pr *gitea.PullRequest) string {
|
||||
head := pr.Head.Name
|
||||
if pr.Head.RepoID != pr.Base.RepoID {
|
||||
if pr.Head.Repository != nil {
|
||||
head = pr.Head.Repository.Owner.UserName + ":" + head
|
||||
} else {
|
||||
head = "delete:" + head
|
||||
}
|
||||
}
|
||||
return head
|
||||
}
|
||||
|
||||
func formatPRState(pr *gitea.PullRequest) string {
|
||||
if pr.Merged != nil {
|
||||
return "merged"
|
||||
}
|
||||
return string(pr.State)
|
||||
}
|
||||
|
||||
func formatCIStatus(ci *gitea.CombinedStatus, machineReadable bool) string {
|
||||
if ci == nil || len(ci.Statuses) == 0 {
|
||||
return ""
|
||||
}
|
||||
if machineReadable {
|
||||
return string(ci.State)
|
||||
}
|
||||
items := make([]string, 0, len(ci.Statuses))
|
||||
for _, s := range ci.Statuses {
|
||||
items = append(items, fmt.Sprintf("%s%s", ciStatusSymbols[s.State], s.Context))
|
||||
}
|
||||
return strings.Join(items, ", ")
|
||||
}
|
||||
|
||||
func formatReviews(pr *gitea.PullRequest, reviews []*gitea.PullReview) string {
|
||||
result := ""
|
||||
if len(reviews) == 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
// deduplicate reviews by user (via review time & userID),
|
||||
reviewByUserOrTeam := make(map[string]*gitea.PullReview)
|
||||
for _, review := range reviews {
|
||||
switch review.State {
|
||||
case gitea.ReviewStateApproved,
|
||||
gitea.ReviewStateRequestChanges,
|
||||
gitea.ReviewStateRequestReview:
|
||||
if review.Reviewer != nil {
|
||||
if r, ok := reviewByUserOrTeam[fmt.Sprintf("user_%d", review.Reviewer.ID)]; !ok || review.Submitted.After(r.Submitted) {
|
||||
reviewByUserOrTeam[fmt.Sprintf("user_%d", review.Reviewer.ID)] = review
|
||||
}
|
||||
} else if review.ReviewerTeam != nil {
|
||||
if r, ok := reviewByUserOrTeam[fmt.Sprintf("team_%d", review.ReviewerTeam.ID)]; !ok || review.Submitted.After(r.Submitted) {
|
||||
reviewByUserOrTeam[fmt.Sprintf("team_%d", review.ReviewerTeam.ID)] = review
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// group reviews by type
|
||||
reviewByState := make(map[gitea.ReviewStateType][]string)
|
||||
for _, r := range reviewByUserOrTeam {
|
||||
if r.Reviewer != nil {
|
||||
reviewByState[r.State] = append(reviewByState[r.State],
|
||||
r.Reviewer.UserName,
|
||||
)
|
||||
} else if r.ReviewerTeam != nil {
|
||||
// only pulls to orgs can have team reviews
|
||||
org := pr.Base.Repository.Owner
|
||||
reviewByState[r.State] = append(reviewByState[r.State],
|
||||
fmt.Sprintf("%s/%s", org.UserName, r.ReviewerTeam.Name),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// stringify
|
||||
for state, user := range reviewByState {
|
||||
result += fmt.Sprintf("- %s by @%s\n", state, strings.Join(user, ", @"))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// PullsList prints a listing of pulls
|
||||
func PullsList(prs []*gitea.PullRequest, output string, fields []string, ciStatuses map[int64]*gitea.CombinedStatus) error {
|
||||
return printPulls(prs, output, fields, ciStatuses)
|
||||
}
|
||||
|
||||
// PullFields are all available fields to print with PullsList()
|
||||
var PullFields = []string{
|
||||
"index",
|
||||
"state",
|
||||
"author",
|
||||
"author-id",
|
||||
"url",
|
||||
|
||||
"title",
|
||||
"body",
|
||||
|
||||
"mergeable",
|
||||
"base",
|
||||
"base-commit",
|
||||
"head",
|
||||
"diff",
|
||||
"patch",
|
||||
|
||||
"created",
|
||||
"updated",
|
||||
"deadline",
|
||||
|
||||
"assignees",
|
||||
"milestone",
|
||||
"labels",
|
||||
"comments",
|
||||
"ci",
|
||||
}
|
||||
|
||||
func printPulls(pulls []*gitea.PullRequest, output string, fields []string, ciStatuses map[int64]*gitea.CombinedStatus) error {
|
||||
labelMap := map[int64]string{}
|
||||
printables := make([]printable, len(pulls))
|
||||
machineReadable := isMachineReadable(output)
|
||||
|
||||
for i, x := range pulls {
|
||||
// pre-serialize labels for performance
|
||||
for _, label := range x.Labels {
|
||||
if _, ok := labelMap[label.ID]; !ok {
|
||||
labelMap[label.ID] = formatLabel(label, !machineReadable, "")
|
||||
}
|
||||
}
|
||||
// store items with printable interface
|
||||
printables[i] = &printablePull{x, &labelMap, &ciStatuses}
|
||||
}
|
||||
|
||||
t := tableFromItems(fields, printables, machineReadable)
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
type printablePull struct {
|
||||
*gitea.PullRequest
|
||||
formattedLabels *map[int64]string
|
||||
ciStatuses *map[int64]*gitea.CombinedStatus
|
||||
}
|
||||
|
||||
func (x printablePull) FormatField(field string, machineReadable bool) string {
|
||||
switch field {
|
||||
case "index":
|
||||
return fmt.Sprintf("%d", x.Index)
|
||||
case "state":
|
||||
return formatPRState(x.PullRequest)
|
||||
case "author":
|
||||
return formatUserName(x.Poster)
|
||||
case "author-id":
|
||||
return x.Poster.UserName
|
||||
case "url":
|
||||
return x.HTMLURL
|
||||
case "title":
|
||||
return x.Title
|
||||
case "body":
|
||||
return x.Body
|
||||
case "created":
|
||||
return FormatTime(*x.Created, machineReadable)
|
||||
case "updated":
|
||||
return FormatTime(*x.Updated, machineReadable)
|
||||
case "deadline":
|
||||
if x.Deadline == nil {
|
||||
return ""
|
||||
}
|
||||
return FormatTime(*x.Deadline, machineReadable)
|
||||
case "milestone":
|
||||
if x.Milestone != nil {
|
||||
return x.Milestone.Title
|
||||
}
|
||||
return ""
|
||||
case "labels":
|
||||
labels := make([]string, len(x.Labels))
|
||||
for i, l := range x.Labels {
|
||||
labels[i] = (*x.formattedLabels)[l.ID]
|
||||
}
|
||||
return strings.Join(labels, " ")
|
||||
case "assignees":
|
||||
assignees := make([]string, len(x.Assignees))
|
||||
for i, a := range x.Assignees {
|
||||
assignees[i] = formatUserName(a)
|
||||
}
|
||||
return strings.Join(assignees, " ")
|
||||
case "comments":
|
||||
return fmt.Sprintf("%d", x.Comments)
|
||||
case "mergeable":
|
||||
isMergeable := x.Mergeable && x.State == gitea.StateOpen
|
||||
return formatBoolean(isMergeable, !machineReadable)
|
||||
case "base":
|
||||
return x.Base.Ref
|
||||
case "base-commit":
|
||||
return x.MergeBase
|
||||
case "head":
|
||||
return formatPRHead(x.PullRequest)
|
||||
case "diff":
|
||||
return x.DiffURL
|
||||
case "patch":
|
||||
return x.PatchURL
|
||||
case "ci":
|
||||
if x.ciStatuses != nil {
|
||||
if ci, ok := (*x.ciStatuses)[x.Index]; ok {
|
||||
return formatCIStatus(ci, machineReadable)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// PullReviewCommentFields are all available fields to print with PullReviewCommentsList()
|
||||
var PullReviewCommentFields = []string{
|
||||
"id",
|
||||
"body",
|
||||
"reviewer",
|
||||
"path",
|
||||
"line",
|
||||
"resolver",
|
||||
"created",
|
||||
"updated",
|
||||
"url",
|
||||
}
|
||||
|
||||
// PullReviewCommentsList prints a listing of pull review comments
|
||||
func PullReviewCommentsList(comments []*gitea.PullReviewComment, output string, fields []string) error {
|
||||
printables := make([]printable, len(comments))
|
||||
for i, c := range comments {
|
||||
printables[i] = &printablePullReviewComment{c}
|
||||
}
|
||||
t := tableFromItems(fields, printables, isMachineReadable(output))
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
type printablePullReviewComment struct {
|
||||
*gitea.PullReviewComment
|
||||
}
|
||||
|
||||
func (x printablePullReviewComment) FormatField(field string, machineReadable bool) string {
|
||||
switch field {
|
||||
case "id":
|
||||
return fmt.Sprintf("%d", x.ID)
|
||||
case "body":
|
||||
return x.Body
|
||||
case "reviewer":
|
||||
if x.Reviewer != nil {
|
||||
return formatUserName(x.Reviewer)
|
||||
}
|
||||
return ""
|
||||
case "path":
|
||||
return x.Path
|
||||
case "line":
|
||||
if x.LineNum != 0 {
|
||||
return fmt.Sprintf("%d", x.LineNum)
|
||||
}
|
||||
if x.OldLineNum != 0 {
|
||||
return fmt.Sprintf("%d", x.OldLineNum)
|
||||
}
|
||||
return ""
|
||||
case "resolver":
|
||||
if x.Resolver != nil {
|
||||
return formatUserName(x.Resolver)
|
||||
}
|
||||
return ""
|
||||
case "created":
|
||||
return FormatTime(x.Created, machineReadable)
|
||||
case "updated":
|
||||
return FormatTime(x.Updated, machineReadable)
|
||||
case "url":
|
||||
return x.HTMLURL
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,200 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestPR(index int64, title string) *gitea.PullRequest {
|
||||
now := time.Now()
|
||||
return &gitea.PullRequest{
|
||||
Index: index,
|
||||
Title: title,
|
||||
State: gitea.StateOpen,
|
||||
Poster: &gitea.User{UserName: "testuser"},
|
||||
Head: &gitea.PRBranchInfo{Ref: "branch", Name: "branch"},
|
||||
Base: &gitea.PRBranchInfo{Ref: "main", Name: "main"},
|
||||
Created: &now,
|
||||
Updated: &now,
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatCIStatusNil(t *testing.T) {
|
||||
assert.Equal(t, "", formatCIStatus(nil, false))
|
||||
assert.Equal(t, "", formatCIStatus(nil, true))
|
||||
}
|
||||
|
||||
func TestFormatCIStatusEmpty(t *testing.T) {
|
||||
ci := &gitea.CombinedStatus{Statuses: []*gitea.Status{}}
|
||||
assert.Equal(t, "", formatCIStatus(ci, false))
|
||||
assert.Equal(t, "", formatCIStatus(ci, true))
|
||||
}
|
||||
|
||||
func TestFormatCIStatusMachineReadable(t *testing.T) {
|
||||
ci := &gitea.CombinedStatus{
|
||||
State: gitea.StatusSuccess,
|
||||
Statuses: []*gitea.Status{
|
||||
{State: gitea.StatusSuccess, Context: "lint"},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, "success", formatCIStatus(ci, true))
|
||||
|
||||
ci.State = gitea.StatusPending
|
||||
ci.Statuses = []*gitea.Status{
|
||||
{State: gitea.StatusPending, Context: "build"},
|
||||
}
|
||||
assert.Equal(t, "pending", formatCIStatus(ci, true))
|
||||
}
|
||||
|
||||
func TestFormatCIStatusSingle(t *testing.T) {
|
||||
ci := &gitea.CombinedStatus{
|
||||
State: gitea.StatusSuccess,
|
||||
Statuses: []*gitea.Status{
|
||||
{State: gitea.StatusSuccess, Context: "lint"},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, "✓ lint", formatCIStatus(ci, false))
|
||||
}
|
||||
|
||||
func TestFormatCIStatusMultiple(t *testing.T) {
|
||||
ci := &gitea.CombinedStatus{
|
||||
State: gitea.StatusFailure,
|
||||
Statuses: []*gitea.Status{
|
||||
{State: gitea.StatusSuccess, Context: "lint"},
|
||||
{State: gitea.StatusPending, Context: "build"},
|
||||
{State: gitea.StatusFailure, Context: "test"},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, "✓ lint, ⏳ build, ❌ test", formatCIStatus(ci, false))
|
||||
}
|
||||
|
||||
func TestFormatCIStatusAllStates(t *testing.T) {
|
||||
tests := []struct {
|
||||
state gitea.StatusState
|
||||
context string
|
||||
expected string
|
||||
}{
|
||||
{gitea.StatusSuccess, "s", "✓ s"},
|
||||
{gitea.StatusPending, "p", "⏳ p"},
|
||||
{gitea.StatusWarning, "w", "⚠ w"},
|
||||
{gitea.StatusError, "e", "✘ e"},
|
||||
{gitea.StatusFailure, "f", "❌ f"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
ci := &gitea.CombinedStatus{
|
||||
State: tt.state,
|
||||
Statuses: []*gitea.Status{{State: tt.state, Context: tt.context}},
|
||||
}
|
||||
assert.Equal(t, tt.expected, formatCIStatus(ci, false), "state: %s", tt.state)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullDetailsIncludesHTMLURL(t *testing.T) {
|
||||
pr := newTestPR(8, "Crm wip")
|
||||
pr.HTMLURL = "https://gitea.example.com/owner/repo/pulls/8"
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
PullDetails(pr, nil, nil)
|
||||
})
|
||||
|
||||
assert.Contains(t, out, pr.HTMLURL)
|
||||
}
|
||||
|
||||
func TestPullsListWithCIField(t *testing.T) {
|
||||
prs := []*gitea.PullRequest{
|
||||
newTestPR(1, "feat: add feature"),
|
||||
newTestPR(2, "fix: bug fix"),
|
||||
}
|
||||
|
||||
ciStatuses := map[int64]*gitea.CombinedStatus{
|
||||
1: {
|
||||
State: gitea.StatusSuccess,
|
||||
Statuses: []*gitea.Status{
|
||||
{State: gitea.StatusSuccess, Context: "ci/build"},
|
||||
},
|
||||
},
|
||||
2: {
|
||||
State: gitea.StatusFailure,
|
||||
Statuses: []*gitea.Status{
|
||||
{State: gitea.StatusFailure, Context: "ci/test"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
tbl := tableFromItems(
|
||||
[]string{"index", "ci"},
|
||||
[]printable{
|
||||
&printablePull{prs[0], &map[int64]string{}, &ciStatuses},
|
||||
&printablePull{prs[1], &map[int64]string{}, &ciStatuses},
|
||||
},
|
||||
true,
|
||||
)
|
||||
require.NoError(t, tbl.fprint(buf, "json"))
|
||||
|
||||
var result []map[string]string
|
||||
require.NoError(t, json.Unmarshal(buf.Bytes(), &result))
|
||||
require.Len(t, result, 2)
|
||||
assert.Equal(t, "1", result[0]["index"])
|
||||
assert.Equal(t, "success", result[0]["ci"])
|
||||
assert.Equal(t, "2", result[1]["index"])
|
||||
assert.Equal(t, "failure", result[1]["ci"])
|
||||
}
|
||||
|
||||
func TestPullsListCIFieldEmpty(t *testing.T) {
|
||||
prs := []*gitea.PullRequest{newTestPR(1, "no ci")}
|
||||
ciStatuses := map[int64]*gitea.CombinedStatus{}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
tbl := tableFromItems(
|
||||
[]string{"index", "ci"},
|
||||
[]printable{
|
||||
&printablePull{prs[0], &map[int64]string{}, &ciStatuses},
|
||||
},
|
||||
true,
|
||||
)
|
||||
require.NoError(t, tbl.fprint(buf, "json"))
|
||||
|
||||
var result []map[string]string
|
||||
require.NoError(t, json.Unmarshal(buf.Bytes(), &result))
|
||||
require.Len(t, result, 1)
|
||||
assert.Equal(t, "", result[0]["ci"])
|
||||
}
|
||||
|
||||
func TestPullsListNilCIStatusesWithCIField(t *testing.T) {
|
||||
prs := []*gitea.PullRequest{newTestPR(1, "nil ci")}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
tbl := tableFromItems(
|
||||
[]string{"index", "ci"},
|
||||
[]printable{
|
||||
&printablePull{prs[0], &map[int64]string{}, nil},
|
||||
},
|
||||
true,
|
||||
)
|
||||
require.NoError(t, tbl.fprint(buf, "json"))
|
||||
|
||||
var result []map[string]string
|
||||
require.NoError(t, json.Unmarshal(buf.Bytes(), &result))
|
||||
require.Len(t, result, 1)
|
||||
assert.Equal(t, "", result[0]["ci"])
|
||||
}
|
||||
|
||||
func TestPullsListNoCIFieldNoPanic(t *testing.T) {
|
||||
prs := []*gitea.PullRequest{newTestPR(1, "test")}
|
||||
require.NoError(t, PullsList(prs, "", []string{"index", "title"}, nil))
|
||||
}
|
||||
|
||||
func TestPullFieldsContainsCI(t *testing.T) {
|
||||
assert.True(t, slices.Contains(PullFields, "ci"), "PullFields should contain 'ci'")
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// ReleasesList prints a listing of releases
|
||||
func ReleasesList(releases []*gitea.Release, output string) error {
|
||||
t := tableWithHeader(
|
||||
"Tag-Name",
|
||||
"Title",
|
||||
"Published At",
|
||||
"Status",
|
||||
"Tar/Zip URL",
|
||||
)
|
||||
|
||||
for _, release := range releases {
|
||||
status := "released"
|
||||
if release.IsDraft {
|
||||
status = "draft"
|
||||
} else if release.IsPrerelease {
|
||||
status = "prerelease"
|
||||
}
|
||||
t.addRow(
|
||||
release.TagName,
|
||||
release.Title,
|
||||
FormatTime(release.PublishedAt, isMachineReadable(output)),
|
||||
status,
|
||||
release.TarURL+"\n"+release.ZipURL,
|
||||
)
|
||||
}
|
||||
|
||||
return t.print(output)
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// ReposList prints a listing of the repos
|
||||
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))
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// RepoDetails print an repo formatted to stdout
|
||||
func RepoDetails(repo *gitea.Repository, topics []string) {
|
||||
title := "# " + repo.FullName
|
||||
if repo.Mirror {
|
||||
title += " (mirror)"
|
||||
}
|
||||
if repo.Fork {
|
||||
title += " (fork)"
|
||||
}
|
||||
if repo.Archived {
|
||||
title += " (archived)"
|
||||
}
|
||||
if repo.Empty {
|
||||
title += " (empty)"
|
||||
}
|
||||
title += "\n"
|
||||
|
||||
var desc string
|
||||
if len(repo.Description) != 0 {
|
||||
desc = fmt.Sprintf("*%s*\n\n", repo.Description)
|
||||
}
|
||||
|
||||
stats := fmt.Sprintf(
|
||||
"Issues: %d, Stars: %d, Forks: %d, Size: %s\n",
|
||||
repo.OpenIssues,
|
||||
repo.Stars,
|
||||
repo.Forks,
|
||||
formatSize(int64(repo.Size)),
|
||||
)
|
||||
|
||||
// NOTE: for mirrors, this is the time the mirror was last fetched..
|
||||
updated := fmt.Sprintf(
|
||||
"Updated: %s (%s ago)\n",
|
||||
repo.Updated.Format("2006-01-02 15:04"),
|
||||
time.Since(repo.Updated).Truncate(time.Minute),
|
||||
)
|
||||
|
||||
urls := fmt.Sprintf(
|
||||
"- Browse:\t%s\n- Clone:\t%s\n",
|
||||
repo.HTMLURL,
|
||||
repo.SSHURL,
|
||||
)
|
||||
if len(repo.Website) != 0 {
|
||||
urls += fmt.Sprintf("- Web:\t%s\n", repo.Website)
|
||||
}
|
||||
|
||||
perm := fmt.Sprintf(
|
||||
"- Permission:\t%s\n",
|
||||
formatPermission(repo.Permissions),
|
||||
)
|
||||
|
||||
var tops string
|
||||
if len(topics) != 0 {
|
||||
tops = fmt.Sprintf("- Topics:\t%s\n", strings.Join(topics, ", "))
|
||||
}
|
||||
|
||||
outputMarkdown(fmt.Sprintf(
|
||||
"%s%s\n%s\n%s%s%s%s",
|
||||
title,
|
||||
desc,
|
||||
stats,
|
||||
updated,
|
||||
urls,
|
||||
perm,
|
||||
tops,
|
||||
), repo.HTMLURL)
|
||||
}
|
||||
|
||||
// RepoFields are the available fields to print with ReposList()
|
||||
var RepoFields = []string{
|
||||
"description",
|
||||
"forks",
|
||||
"id",
|
||||
"name",
|
||||
"owner",
|
||||
"stars",
|
||||
"ssh",
|
||||
"updated",
|
||||
"url",
|
||||
"permission",
|
||||
"type",
|
||||
}
|
||||
|
||||
type printableRepo struct{ *gitea.Repository }
|
||||
|
||||
func (x printableRepo) FormatField(field string, machineReadable bool) string {
|
||||
switch field {
|
||||
case "description":
|
||||
return x.Description
|
||||
case "forks":
|
||||
return fmt.Sprintf("%d", x.Forks)
|
||||
case "id":
|
||||
return fmt.Sprintf("%d", x.ID)
|
||||
case "name":
|
||||
return x.Name
|
||||
case "owner":
|
||||
return x.Owner.UserName
|
||||
case "stars":
|
||||
return fmt.Sprintf("%d", x.Stars)
|
||||
case "ssh":
|
||||
return x.SSHURL
|
||||
case "updated":
|
||||
return FormatTime(x.Updated, machineReadable)
|
||||
case "url":
|
||||
return x.HTMLURL
|
||||
case "permission":
|
||||
return formatPermission(x.Permissions)
|
||||
case "type":
|
||||
if x.Fork {
|
||||
return "fork"
|
||||
}
|
||||
if x.Mirror {
|
||||
return "mirror"
|
||||
}
|
||||
return "source"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
"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"])
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// SSHKeysList prints a table of SSH public keys
|
||||
func SSHKeysList(keys []*gitea.PublicKey, output string) error {
|
||||
if len(keys) == 0 {
|
||||
fmt.Printf("No SSH keys found\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
t := tableWithHeader(
|
||||
"ID",
|
||||
"Title",
|
||||
"Fingerprint",
|
||||
"KeyType",
|
||||
"ReadOnly",
|
||||
"Created",
|
||||
)
|
||||
|
||||
for _, k := range keys {
|
||||
readOnly := "false"
|
||||
if k.ReadOnly {
|
||||
readOnly = "true"
|
||||
}
|
||||
t.addRow(
|
||||
fmt.Sprintf("%d", k.ID),
|
||||
k.Title,
|
||||
k.Fingerprint,
|
||||
k.KeyType,
|
||||
readOnly,
|
||||
FormatTime(k.Created, false),
|
||||
)
|
||||
}
|
||||
|
||||
return t.print(output)
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// table provides infrastructure to easily print (sorted) lists in different formats
|
||||
type table struct {
|
||||
headers []string
|
||||
values [][]string
|
||||
sortDesc bool // used internally by sortable interface
|
||||
sortColumn uint // ↑
|
||||
}
|
||||
|
||||
// printable can be implemented for structs to put fields dynamically into a table
|
||||
type printable interface {
|
||||
FormatField(field string, machineReadable bool) string
|
||||
}
|
||||
|
||||
// high level api to print a table of items with dynamic fields
|
||||
func tableFromItems(fields []string, values []printable, machineReadable bool) table {
|
||||
t := table{headers: fields}
|
||||
for _, v := range values {
|
||||
row := make([]string, len(fields))
|
||||
for i, f := range fields {
|
||||
row[i] = v.FormatField(f, machineReadable)
|
||||
}
|
||||
t.addRowSlice(row)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func tableWithHeader(header ...string) table {
|
||||
return table{headers: header}
|
||||
}
|
||||
|
||||
// it's the callers responsibility to ensure row length is equal to header length!
|
||||
func (t *table) addRow(row ...string) {
|
||||
t.addRowSlice(row)
|
||||
}
|
||||
|
||||
// it's the callers responsibility to ensure row length is equal to header length!
|
||||
func (t *table) addRowSlice(row []string) {
|
||||
t.values = append(t.values, row)
|
||||
}
|
||||
|
||||
func (t *table) sort(column uint, desc bool) {
|
||||
t.sortColumn = column
|
||||
t.sortDesc = desc
|
||||
sort.Stable(t) // stable to allow multiple calls to sort
|
||||
}
|
||||
|
||||
// sortable interface
|
||||
func (t table) Len() int { return len(t.values) }
|
||||
func (t table) Swap(i, j int) { t.values[i], t.values[j] = t.values[j], t.values[i] }
|
||||
func (t table) Less(i, j int) bool {
|
||||
if t.sortDesc {
|
||||
i, j = j, i
|
||||
}
|
||||
return t.values[i][t.sortColumn] < t.values[j][t.sortColumn]
|
||||
}
|
||||
|
||||
func (t *table) print(output string) error {
|
||||
return t.fprint(os.Stdout, output)
|
||||
}
|
||||
|
||||
func (t *table) fprint(f io.Writer, output string) error {
|
||||
switch output {
|
||||
case "", "table":
|
||||
return outputTable(f, t.headers, t.values)
|
||||
case "csv":
|
||||
return outputDsv(f, t.headers, t.values, ',')
|
||||
case "simple":
|
||||
return outputSimple(f, t.headers, t.values)
|
||||
case "tsv":
|
||||
return outputDsv(f, t.headers, t.values, '\t')
|
||||
case "yml", "yaml":
|
||||
return outputYaml(f, t.headers, t.values)
|
||||
case "json":
|
||||
return outputJSON(f, t.headers, t.values)
|
||||
default:
|
||||
return fmt.Errorf("unknown output type %q, available types are: csv, simple, table, tsv, yaml, json", output)
|
||||
}
|
||||
}
|
||||
|
||||
// outputTable prints structured data as table
|
||||
func outputTable(f io.Writer, headers []string, values [][]string) error {
|
||||
table := tablewriter.NewWriter(f)
|
||||
if len(headers) > 0 {
|
||||
table.Header(headers)
|
||||
}
|
||||
for _, value := range values {
|
||||
if err := table.Append(value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return table.Render()
|
||||
}
|
||||
|
||||
// outputSimple prints structured data as space delimited value
|
||||
func outputSimple(f io.Writer, headers []string, values [][]string) error {
|
||||
for _, value := range values {
|
||||
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, delimiter rune) error {
|
||||
writer := csv.NewWriter(f)
|
||||
writer.Comma = delimiter
|
||||
if err := writer.Write(headers); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, value := range values {
|
||||
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) error {
|
||||
root := &yaml.Node{Kind: yaml.SequenceNode}
|
||||
for _, value := range values {
|
||||
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}
|
||||
if _, err := strconv.ParseInt(val, 10, 64); err == nil {
|
||||
valueNode.Tag = "!!int"
|
||||
} else {
|
||||
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 (
|
||||
matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
|
||||
matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
|
||||
)
|
||||
|
||||
func toSnakeCase(str string) string {
|
||||
snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
|
||||
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
|
||||
return strings.ToLower(snake)
|
||||
}
|
||||
|
||||
// orderedRow preserves header insertion order when marshaled to JSON.
|
||||
type orderedRow struct {
|
||||
keys []string
|
||||
values map[string]string
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
switch outputFormat {
|
||||
case "yml", "yaml", "csv", "tsv", "json":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
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) {
|
||||
assert.EqualValues(t, "some_test_var_at2d", toSnakeCase("SomeTestVarAt2d"))
|
||||
}
|
||||
|
||||
func TestPrint(t *testing.T) {
|
||||
tData := &table{
|
||||
headers: []string{"A", "B"},
|
||||
values: [][]string{
|
||||
{"new a", "some bbbb"},
|
||||
{"AAAAA", "b2"},
|
||||
{"\"abc", "\"def"},
|
||||
{"'abc", "de'f"},
|
||||
{"\\abc", "'def\\"},
|
||||
},
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
require.NoError(t, tData.fprint(buf, "json"))
|
||||
result := []struct {
|
||||
A string
|
||||
B string
|
||||
}{}
|
||||
assert.NoError(t, json.NewDecoder(buf).Decode(&result))
|
||||
|
||||
if assert.Len(t, result, 5) {
|
||||
assert.EqualValues(t, "new a", result[0].A)
|
||||
assert.EqualValues(t, "some bbbb", result[0].B)
|
||||
assert.EqualValues(t, "AAAAA", result[1].A)
|
||||
assert.EqualValues(t, "b2", result[1].B)
|
||||
assert.EqualValues(t, "\"abc", result[2].A)
|
||||
assert.EqualValues(t, "\"def", result[2].B)
|
||||
assert.EqualValues(t, "'abc", result[3].A)
|
||||
assert.EqualValues(t, "de'f", result[3].B)
|
||||
assert.EqualValues(t, "\\abc", result[4].A)
|
||||
assert.EqualValues(t, "'def\\", result[4].B)
|
||||
}
|
||||
|
||||
buf.Reset()
|
||||
|
||||
require.NoError(t, tData.fprint(buf, "yaml"))
|
||||
|
||||
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"`)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// TrackedTimesList print list of tracked times to stdout
|
||||
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 {
|
||||
totalDuration += t.Time
|
||||
printables[i] = &printableTrackedTime{t, outputType}
|
||||
}
|
||||
t := tableFromItems(fields, printables, isMachineReadable(outputType))
|
||||
|
||||
if printTotal {
|
||||
total := make([]string, len(fields))
|
||||
total[0] = "TOTAL"
|
||||
total[len(fields)-1] = formatDuration(totalDuration, outputType)
|
||||
t.addRowSlice(total)
|
||||
}
|
||||
|
||||
return t.print(outputType)
|
||||
}
|
||||
|
||||
// TrackedTimeFields contains all available fields for printing of tracked times.
|
||||
var TrackedTimeFields = []string{
|
||||
"id",
|
||||
"created",
|
||||
"repo",
|
||||
"issue",
|
||||
"user",
|
||||
"duration",
|
||||
}
|
||||
|
||||
type printableTrackedTime struct {
|
||||
*gitea.TrackedTime
|
||||
outputFormat string
|
||||
}
|
||||
|
||||
func (t printableTrackedTime) FormatField(field string, machineReadable bool) string {
|
||||
switch field {
|
||||
case "id":
|
||||
return fmt.Sprintf("%d", t.ID)
|
||||
case "created":
|
||||
return FormatTime(t.Created, machineReadable)
|
||||
case "repo":
|
||||
return t.Issue.Repository.FullName
|
||||
case "issue":
|
||||
return fmt.Sprintf("#%d", t.Issue.Index)
|
||||
case "user":
|
||||
return t.UserName
|
||||
case "duration":
|
||||
return formatDuration(t.Time, t.outputFormat)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// UserDetails print a formatted user to stdout
|
||||
func UserDetails(user *gitea.User) {
|
||||
title := "# " + user.UserName
|
||||
if user.IsAdmin {
|
||||
title += " (admin)"
|
||||
}
|
||||
if !user.IsActive {
|
||||
title += " (disabled)"
|
||||
}
|
||||
if user.Restricted {
|
||||
title += " (restricted)"
|
||||
}
|
||||
if user.ProhibitLogin {
|
||||
title += " (login prohibited)"
|
||||
}
|
||||
title += "\n"
|
||||
|
||||
var desc string
|
||||
if len(user.Description) != 0 {
|
||||
desc = fmt.Sprintf("*%s*\n\n", user.Description)
|
||||
}
|
||||
var website string
|
||||
if len(user.Website) != 0 {
|
||||
website = fmt.Sprintf("%s\n\n", user.Website)
|
||||
}
|
||||
|
||||
stats := fmt.Sprintf(
|
||||
"Follower Count: %d, Following Count: %d, Starred Repos: %d\n",
|
||||
user.FollowerCount,
|
||||
user.FollowingCount,
|
||||
user.StarredRepoCount,
|
||||
)
|
||||
|
||||
outputMarkdown(fmt.Sprintf(
|
||||
"%s%s\n%s\n%s",
|
||||
title,
|
||||
desc,
|
||||
website,
|
||||
stats,
|
||||
), "")
|
||||
}
|
||||
|
||||
// UserList prints a listing of the users
|
||||
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))
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// UserFields are the available fields to print with UserList()
|
||||
var UserFields = []string{
|
||||
"id",
|
||||
"login",
|
||||
"full_name",
|
||||
"email",
|
||||
"avatar_url",
|
||||
"language",
|
||||
"is_admin",
|
||||
"restricted",
|
||||
"prohibit_login",
|
||||
"location",
|
||||
"website",
|
||||
"description",
|
||||
"visibility",
|
||||
"activated",
|
||||
"lastlogin_at",
|
||||
"created_at",
|
||||
}
|
||||
|
||||
type printableUser struct{ *gitea.User }
|
||||
|
||||
func (x printableUser) FormatField(field string, machineReadable bool) string {
|
||||
switch field {
|
||||
case "id":
|
||||
return fmt.Sprintf("%d", x.ID)
|
||||
case "login":
|
||||
if x.IsAdmin {
|
||||
return fmt.Sprintf("%s (admin)", x.UserName)
|
||||
}
|
||||
if !x.IsActive {
|
||||
return fmt.Sprintf("%s (disabled)", x.UserName)
|
||||
}
|
||||
if x.Restricted {
|
||||
return fmt.Sprintf("%s (restricted)", x.UserName)
|
||||
}
|
||||
if x.ProhibitLogin {
|
||||
return fmt.Sprintf("%s (login prohibited)", x.UserName)
|
||||
}
|
||||
return x.UserName
|
||||
case "full_name":
|
||||
return x.FullName
|
||||
case "email":
|
||||
return x.Email
|
||||
case "avatar_url":
|
||||
return x.AvatarURL
|
||||
case "language":
|
||||
return x.Language
|
||||
case "is_admin":
|
||||
return formatBoolean(x.IsAdmin, !machineReadable)
|
||||
case "restricted":
|
||||
return formatBoolean(x.Restricted, !machineReadable)
|
||||
case "prohibit_login":
|
||||
return formatBoolean(x.ProhibitLogin, !machineReadable)
|
||||
case "activated":
|
||||
return formatBoolean(x.IsActive, !machineReadable)
|
||||
case "location":
|
||||
return x.Location
|
||||
case "website":
|
||||
return x.Website
|
||||
case "description":
|
||||
return x.Description
|
||||
case "visibility":
|
||||
return string(x.Visibility)
|
||||
case "created_at":
|
||||
return FormatTime(x.Created, machineReadable)
|
||||
case "lastlogin_at":
|
||||
return FormatTime(x.LastLogin, machineReadable)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// WebhooksList prints a listing of webhooks
|
||||
func WebhooksList(hooks []*gitea.Hook, output string) error {
|
||||
t := tableWithHeader(
|
||||
"ID",
|
||||
"Type",
|
||||
"URL",
|
||||
"Events",
|
||||
"Active",
|
||||
"Updated",
|
||||
)
|
||||
|
||||
for _, hook := range hooks {
|
||||
var url string
|
||||
if hook.Config != nil {
|
||||
url = hook.Config["url"]
|
||||
}
|
||||
|
||||
events := strings.Join(hook.Events, ",")
|
||||
if len(events) > 40 {
|
||||
events = events[:37] + "..."
|
||||
}
|
||||
|
||||
active := "✓"
|
||||
if !hook.Active {
|
||||
active = "✗"
|
||||
}
|
||||
|
||||
t.addRow(
|
||||
strconv.FormatInt(hook.ID, 10),
|
||||
string(hook.Type),
|
||||
url,
|
||||
events,
|
||||
active,
|
||||
FormatTime(hook.Updated, false),
|
||||
)
|
||||
}
|
||||
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// WebhookDetails prints detailed information about a webhook
|
||||
func WebhookDetails(hook *gitea.Hook) {
|
||||
fmt.Printf("# Webhook %d\n\n", hook.ID)
|
||||
fmt.Printf("- **Type**: %s\n", hook.Type)
|
||||
fmt.Printf("- **Active**: %t\n", hook.Active)
|
||||
fmt.Printf("- **Created**: %s\n", FormatTime(hook.Created, false))
|
||||
fmt.Printf("- **Updated**: %s\n", FormatTime(hook.Updated, false))
|
||||
|
||||
if hook.Config != nil {
|
||||
fmt.Printf("- **URL**: %s\n", hook.Config["url"])
|
||||
if contentType, ok := hook.Config["content_type"]; ok {
|
||||
fmt.Printf("- **Content Type**: %s\n", contentType)
|
||||
}
|
||||
if method, ok := hook.Config["http_method"]; ok {
|
||||
fmt.Printf("- **HTTP Method**: %s\n", method)
|
||||
}
|
||||
branchFilter := hook.BranchFilter
|
||||
if branchFilter == "" {
|
||||
branchFilter = hook.Config["branch_filter"]
|
||||
}
|
||||
if branchFilter != "" {
|
||||
fmt.Printf("- **Branch Filter**: %s\n", branchFilter)
|
||||
}
|
||||
if _, hasSecret := hook.Config["secret"]; hasSecret {
|
||||
fmt.Printf("- **Secret**: (configured)\n")
|
||||
}
|
||||
hasAuth := hook.AuthorizationHeader != ""
|
||||
if !hasAuth {
|
||||
_, hasAuth = hook.Config["authorization_header"]
|
||||
}
|
||||
if hasAuth {
|
||||
fmt.Printf("- **Authorization Header**: (configured)\n")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("- **Events**: %s\n", strings.Join(hook.Events, ", "))
|
||||
}
|
||||
@@ -1,384 +0,0 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestWebhooksList(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
hooks := []*gitea.Hook{
|
||||
{
|
||||
ID: 1,
|
||||
Type: "gitea",
|
||||
Config: map[string]string{
|
||||
"url": "https://example.com/webhook",
|
||||
},
|
||||
Events: []string{"push", "pull_request"},
|
||||
Active: true,
|
||||
Updated: now,
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Type: "slack",
|
||||
Config: map[string]string{
|
||||
"url": "https://hooks.slack.com/services/xxx",
|
||||
},
|
||||
Events: []string{"push"},
|
||||
Active: false,
|
||||
Updated: now,
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Type: "discord",
|
||||
Config: nil,
|
||||
Events: []string{"pull_request", "pull_request_review_approved"},
|
||||
Active: true,
|
||||
Updated: now,
|
||||
},
|
||||
}
|
||||
|
||||
// Test that function doesn't panic with various output formats
|
||||
outputFormats := []string{"table", "csv", "json", "yaml", "simple", "tsv"}
|
||||
|
||||
for _, format := range outputFormats {
|
||||
t.Run("Format_"+format, func(t *testing.T) {
|
||||
assert.NoError(t, WebhooksList(hooks, format))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhooksListEmpty(t *testing.T) {
|
||||
// Test with empty hook list
|
||||
hooks := []*gitea.Hook{}
|
||||
|
||||
assert.NoError(t, WebhooksList(hooks, "table"))
|
||||
}
|
||||
|
||||
func TestWebhooksListNil(t *testing.T) {
|
||||
// Test with nil hook list
|
||||
assert.NoError(t, WebhooksList(nil, "table"))
|
||||
}
|
||||
|
||||
func TestWebhookDetails(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
hook *gitea.Hook
|
||||
}{
|
||||
{
|
||||
name: "Complete webhook",
|
||||
hook: &gitea.Hook{
|
||||
ID: 123,
|
||||
Type: "gitea",
|
||||
Config: map[string]string{
|
||||
"url": "https://example.com/webhook",
|
||||
"content_type": "json",
|
||||
"http_method": "post",
|
||||
"secret": "secret-value",
|
||||
},
|
||||
BranchFilter: "main,develop",
|
||||
AuthorizationHeader: "Bearer token123",
|
||||
Events: []string{"push", "pull_request", "issues"},
|
||||
Active: true,
|
||||
Created: now.Add(-24 * time.Hour),
|
||||
Updated: now,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Minimal webhook",
|
||||
hook: &gitea.Hook{
|
||||
ID: 456,
|
||||
Type: "slack",
|
||||
Config: map[string]string{"url": "https://hooks.slack.com/xxx"},
|
||||
Events: []string{"push"},
|
||||
Active: false,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Webhook with nil config",
|
||||
hook: &gitea.Hook{
|
||||
ID: 789,
|
||||
Type: "discord",
|
||||
Config: nil,
|
||||
Events: []string{"pull_request"},
|
||||
Active: true,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Webhook with empty config",
|
||||
hook: &gitea.Hook{
|
||||
ID: 999,
|
||||
Type: "gitea",
|
||||
Config: map[string]string{},
|
||||
Events: []string{},
|
||||
Active: false,
|
||||
Created: now,
|
||||
Updated: now,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Should not panic
|
||||
assert.NotPanics(t, func() {
|
||||
WebhookDetails(tt.hook)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookEventsTruncation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
events []string
|
||||
maxLength int
|
||||
shouldTruncate bool
|
||||
}{
|
||||
{
|
||||
name: "Short events list",
|
||||
events: []string{"push"},
|
||||
maxLength: 40,
|
||||
shouldTruncate: false,
|
||||
},
|
||||
{
|
||||
name: "Medium events list",
|
||||
events: []string{"push", "pull_request"},
|
||||
maxLength: 40,
|
||||
shouldTruncate: false,
|
||||
},
|
||||
{
|
||||
name: "Long events list",
|
||||
events: []string{"push", "pull_request", "pull_request_review_approved", "pull_request_sync", "issues"},
|
||||
maxLength: 40,
|
||||
shouldTruncate: true,
|
||||
},
|
||||
{
|
||||
name: "Very long events list",
|
||||
events: []string{"push", "pull_request", "pull_request_review_approved", "pull_request_review_rejected", "pull_request_comment", "pull_request_assigned", "pull_request_label"},
|
||||
maxLength: 40,
|
||||
shouldTruncate: true,
|
||||
},
|
||||
{
|
||||
name: "Empty events",
|
||||
events: []string{},
|
||||
maxLength: 40,
|
||||
shouldTruncate: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
eventsStr := strings.Join(tt.events, ",")
|
||||
|
||||
if len(eventsStr) > tt.maxLength {
|
||||
assert.True(t, tt.shouldTruncate, "Events string should be truncated")
|
||||
truncated := eventsStr[:tt.maxLength-3] + "..."
|
||||
assert.Contains(t, truncated, "...")
|
||||
assert.LessOrEqual(t, len(truncated), tt.maxLength)
|
||||
} else {
|
||||
assert.False(t, tt.shouldTruncate, "Events string should not be truncated")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookActiveStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
active bool
|
||||
expectedSymbol string
|
||||
}{
|
||||
{
|
||||
name: "Active webhook",
|
||||
active: true,
|
||||
expectedSymbol: "✓",
|
||||
},
|
||||
{
|
||||
name: "Inactive webhook",
|
||||
active: false,
|
||||
expectedSymbol: "✗",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
symbol := "✓"
|
||||
if !tt.active {
|
||||
symbol = "✗"
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expectedSymbol, symbol)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookConfigHandling(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config map[string]string
|
||||
expectedURL string
|
||||
hasSecret bool
|
||||
hasAuthHeader bool
|
||||
}{
|
||||
{
|
||||
name: "Config with all fields",
|
||||
config: map[string]string{
|
||||
"url": "https://example.com/webhook",
|
||||
"secret": "my-secret",
|
||||
"content_type": "json",
|
||||
"http_method": "post",
|
||||
},
|
||||
expectedURL: "https://example.com/webhook",
|
||||
hasSecret: true,
|
||||
hasAuthHeader: false,
|
||||
},
|
||||
{
|
||||
name: "Config with minimal fields",
|
||||
config: map[string]string{
|
||||
"url": "https://hooks.slack.com/xxx",
|
||||
},
|
||||
expectedURL: "https://hooks.slack.com/xxx",
|
||||
hasSecret: false,
|
||||
hasAuthHeader: false,
|
||||
},
|
||||
{
|
||||
name: "Nil config",
|
||||
config: nil,
|
||||
expectedURL: "",
|
||||
hasSecret: false,
|
||||
hasAuthHeader: false,
|
||||
},
|
||||
{
|
||||
name: "Empty config",
|
||||
config: map[string]string{},
|
||||
expectedURL: "",
|
||||
hasSecret: false,
|
||||
hasAuthHeader: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var url string
|
||||
if tt.config != nil {
|
||||
url = tt.config["url"]
|
||||
}
|
||||
assert.Equal(t, tt.expectedURL, url)
|
||||
|
||||
var hasSecret, hasAuthHeader bool
|
||||
if tt.config != nil {
|
||||
_, hasSecret = tt.config["secret"]
|
||||
_, hasAuthHeader = tt.config["authorization_header"]
|
||||
}
|
||||
assert.Equal(t, tt.hasSecret, hasSecret)
|
||||
assert.Equal(t, tt.hasAuthHeader, hasAuthHeader)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookTableHeaders(t *testing.T) {
|
||||
expectedHeaders := []string{
|
||||
"ID",
|
||||
"Type",
|
||||
"URL",
|
||||
"Events",
|
||||
"Active",
|
||||
"Updated",
|
||||
}
|
||||
|
||||
// Verify all headers are non-empty and unique
|
||||
headerSet := make(map[string]bool)
|
||||
for _, header := range expectedHeaders {
|
||||
assert.NotEmpty(t, header, "Header should not be empty")
|
||||
assert.False(t, headerSet[header], "Header %s should be unique", header)
|
||||
headerSet[header] = true
|
||||
}
|
||||
|
||||
assert.Len(t, expectedHeaders, 6, "Should have exactly 6 headers")
|
||||
}
|
||||
|
||||
func TestWebhookTypeValues(t *testing.T) {
|
||||
validTypes := []string{
|
||||
"gitea",
|
||||
"gogs",
|
||||
"slack",
|
||||
"discord",
|
||||
"dingtalk",
|
||||
"telegram",
|
||||
"msteams",
|
||||
"feishu",
|
||||
"wechatwork",
|
||||
"packagist",
|
||||
}
|
||||
|
||||
for _, hookType := range validTypes {
|
||||
t.Run("Type_"+hookType, func(t *testing.T) {
|
||||
assert.NotEmpty(t, hookType, "Hook type should not be empty")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookDetailsFormatting(t *testing.T) {
|
||||
now := time.Now()
|
||||
hook := &gitea.Hook{
|
||||
ID: 123,
|
||||
Type: "gitea",
|
||||
Config: map[string]string{
|
||||
"url": "https://example.com/webhook",
|
||||
"content_type": "json",
|
||||
"http_method": "post",
|
||||
"secret": "secret-value",
|
||||
},
|
||||
BranchFilter: "main,develop",
|
||||
AuthorizationHeader: "Bearer token123",
|
||||
Events: []string{"push", "pull_request", "issues"},
|
||||
Active: true,
|
||||
Created: now.Add(-24 * time.Hour),
|
||||
Updated: now,
|
||||
}
|
||||
|
||||
// Test that all expected fields are included in details
|
||||
expectedElements := []string{
|
||||
"123", // webhook ID
|
||||
"gitea", // webhook type
|
||||
"true", // active status
|
||||
"https://example.com/webhook", // URL
|
||||
"json", // content type
|
||||
"post", // HTTP method
|
||||
"main,develop", // branch filter
|
||||
"(configured)", // secret indicator
|
||||
"(configured)", // auth header indicator
|
||||
"push, pull_request, issues", // events list
|
||||
}
|
||||
|
||||
// Verify elements exist (placeholder test)
|
||||
assert.Greater(t, len(expectedElements), 0, "Should have expected elements")
|
||||
|
||||
// This is a functional test - in practice, we'd capture output
|
||||
// For now, we verify the webhook structure contains expected data
|
||||
assert.Equal(t, int64(123), hook.ID)
|
||||
assert.Equal(t, "gitea", hook.Type)
|
||||
assert.True(t, hook.Active)
|
||||
assert.Equal(t, "https://example.com/webhook", hook.Config["url"])
|
||||
assert.Equal(t, "json", hook.Config["content_type"])
|
||||
assert.Equal(t, "post", hook.Config["http_method"])
|
||||
assert.Equal(t, "main,develop", hook.BranchFilter)
|
||||
assert.Contains(t, hook.Config, "secret")
|
||||
assert.Equal(t, "Bearer token123", hook.AuthorizationHeader)
|
||||
assert.Equal(t, []string{"push", "pull_request", "issues"}, hook.Events)
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
)
|
||||
|
||||
// WikiPageFields are all available fields to print with WikiPagesList().
|
||||
var WikiPageFields = []string{
|
||||
"title",
|
||||
"path",
|
||||
"url",
|
||||
"sha",
|
||||
"author",
|
||||
"updated",
|
||||
"message",
|
||||
}
|
||||
|
||||
// WikiRevisionFields are all available fields to print with WikiRevisionsList().
|
||||
var WikiRevisionFields = []string{
|
||||
"sha",
|
||||
"message",
|
||||
"author",
|
||||
"date",
|
||||
}
|
||||
|
||||
// WikiPagesList prints a listing of wiki pages.
|
||||
func WikiPagesList(pages []*gitea.WikiPageMetaData, output string, fields []string) error {
|
||||
if len(pages) == 0 && !isMachineReadable(output) {
|
||||
fmt.Println("No wiki pages found")
|
||||
return nil
|
||||
}
|
||||
|
||||
t := wikiPagesTable(pages, fields, isMachineReadable(output))
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// WikiPageDetails prints a wiki page.
|
||||
func WikiPageDetails(page *gitea.WikiPage, output string) error {
|
||||
if output == "" {
|
||||
markdown, err := renderWikiPageMarkdown(page)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return outputMarkdown(markdown, getRepoURL(page.HTMLURL))
|
||||
}
|
||||
|
||||
t := wikiPageDetailsTable(page, isMachineReadable(output))
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
// WikiRevisionsList prints a listing of wiki page revisions.
|
||||
func WikiRevisionsList(revisions []*gitea.WikiCommit, output string, fields []string) error {
|
||||
t := wikiRevisionsTable(revisions, fields, isMachineReadable(output))
|
||||
return t.print(output)
|
||||
}
|
||||
|
||||
func wikiPagesTable(pages []*gitea.WikiPageMetaData, fields []string, machineReadable bool) table {
|
||||
printables := make([]printable, len(pages))
|
||||
for i, page := range pages {
|
||||
printables[i] = printableWikiPageMetaData{page}
|
||||
}
|
||||
return tableFromItems(fields, printables, machineReadable)
|
||||
}
|
||||
|
||||
func wikiPageDetailsTable(page *gitea.WikiPage, machineReadable bool) table {
|
||||
content, _ := decodeWikiContent(page.ContentBase64)
|
||||
fields := []string{"title", "content", "commits", "path", "url", "sha", "author", "updated", "message"}
|
||||
return tableFromItems(fields, []printable{printableWikiPage{page: page, content: content}}, machineReadable)
|
||||
}
|
||||
|
||||
func wikiRevisionsTable(revisions []*gitea.WikiCommit, fields []string, machineReadable bool) table {
|
||||
printables := make([]printable, len(revisions))
|
||||
for i, revision := range revisions {
|
||||
printables[i] = printableWikiRevision{revision}
|
||||
}
|
||||
return tableFromItems(fields, printables, machineReadable)
|
||||
}
|
||||
|
||||
func renderWikiPageMarkdown(page *gitea.WikiPage) (string, error) {
|
||||
content, err := decodeWikiContent(page.ContentBase64)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !strings.HasSuffix(content, "\n") {
|
||||
content += "\n"
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
func decodeWikiContent(contentBase64 string) (string, error) {
|
||||
decoded, err := base64.StdEncoding.DecodeString(contentBase64)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(decoded), nil
|
||||
}
|
||||
|
||||
type printableWikiPageMetaData struct{ *gitea.WikiPageMetaData }
|
||||
|
||||
type printableWikiPage struct {
|
||||
page *gitea.WikiPage
|
||||
content string
|
||||
}
|
||||
|
||||
type printableWikiRevision struct{ *gitea.WikiCommit }
|
||||
|
||||
func (x printableWikiPageMetaData) FormatField(field string, machineReadable bool) string {
|
||||
switch field {
|
||||
case "title":
|
||||
return x.Title
|
||||
case "path":
|
||||
return x.SubURL
|
||||
case "url":
|
||||
return x.HTMLURL
|
||||
case "sha":
|
||||
return shortWikiCommitID(x.LastCommit)
|
||||
case "author":
|
||||
return wikiCommitAuthor(x.LastCommit)
|
||||
case "updated":
|
||||
return wikiCommitDate(x.LastCommit, machineReadable)
|
||||
case "message":
|
||||
return wikiCommitMessage(x.LastCommit)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x printableWikiPage) FormatField(field string, machineReadable bool) string {
|
||||
switch field {
|
||||
case "title":
|
||||
return x.page.Title
|
||||
case "content":
|
||||
return x.content
|
||||
case "commits":
|
||||
return fmt.Sprintf("%d", x.page.CommitCount)
|
||||
case "path":
|
||||
return x.page.SubURL
|
||||
case "url":
|
||||
return x.page.HTMLURL
|
||||
case "sha":
|
||||
return shortWikiCommitID(x.page.LastCommit)
|
||||
case "author":
|
||||
return wikiCommitAuthor(x.page.LastCommit)
|
||||
case "updated":
|
||||
return wikiCommitDate(x.page.LastCommit, machineReadable)
|
||||
case "message":
|
||||
return wikiCommitMessage(x.page.LastCommit)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x printableWikiRevision) FormatField(field string, machineReadable bool) string {
|
||||
switch field {
|
||||
case "sha":
|
||||
return shortWikiCommitID(x.WikiCommit)
|
||||
case "message":
|
||||
return x.Message
|
||||
case "author":
|
||||
return wikiCommitAuthor(x.WikiCommit)
|
||||
case "date":
|
||||
return wikiCommitDate(x.WikiCommit, machineReadable)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func shortWikiCommitID(commit *gitea.WikiCommit) string {
|
||||
if commit == nil {
|
||||
return ""
|
||||
}
|
||||
if len(commit.ID) <= 7 {
|
||||
return commit.ID
|
||||
}
|
||||
return commit.ID[:7]
|
||||
}
|
||||
|
||||
func wikiCommitAuthor(commit *gitea.WikiCommit) string {
|
||||
if commit == nil || commit.Author == nil {
|
||||
return ""
|
||||
}
|
||||
if commit.Author.Name != "" {
|
||||
return commit.Author.Name
|
||||
}
|
||||
return commit.Author.Email
|
||||
}
|
||||
|
||||
func wikiCommitDate(commit *gitea.WikiCommit, machineReadable bool) string {
|
||||
if commit == nil || commit.Author == nil || commit.Author.Date == "" {
|
||||
return ""
|
||||
}
|
||||
parsed, err := time.Parse(time.RFC3339, commit.Author.Date)
|
||||
if err != nil {
|
||||
return commit.Author.Date
|
||||
}
|
||||
return FormatTime(parsed, machineReadable)
|
||||
}
|
||||
|
||||
func wikiCommitMessage(commit *gitea.WikiCommit) string {
|
||||
if commit == nil {
|
||||
return ""
|
||||
}
|
||||
return commit.Message
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package print
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"gitea.dev/sdk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWikiPagesTableJSON(t *testing.T) {
|
||||
pages := []*gitea.WikiPageMetaData{{
|
||||
Title: "Home",
|
||||
HTMLURL: "https://gitea.example.com/octo/tea/wiki/Home",
|
||||
SubURL: "/octo/tea/wiki/Home",
|
||||
LastCommit: &gitea.WikiCommit{
|
||||
ID: "abcdef123456",
|
||||
Message: "update home",
|
||||
Author: &gitea.CommitUser{GitIdentity: gitea.GitIdentity{Name: "alice"}, Date: "2026-05-21T01:02:03Z"},
|
||||
},
|
||||
}}
|
||||
|
||||
tbl := wikiPagesTable(pages, []string{"title", "author", "updated", "sha", "url"}, true)
|
||||
buf := &bytes.Buffer{}
|
||||
require.NoError(t, tbl.fprint(buf, "json"))
|
||||
|
||||
var rows []map[string]string
|
||||
require.NoError(t, json.Unmarshal(buf.Bytes(), &rows))
|
||||
require.Len(t, rows, 1)
|
||||
assert.Equal(t, "Home", rows[0]["title"])
|
||||
assert.Equal(t, "alice", rows[0]["author"])
|
||||
assert.Equal(t, "2026-05-21T01:02:03Z", rows[0]["updated"])
|
||||
assert.Equal(t, "abcdef1", rows[0]["sha"])
|
||||
assert.Equal(t, "https://gitea.example.com/octo/tea/wiki/Home", rows[0]["url"])
|
||||
}
|
||||
|
||||
func TestWikiRevisionsTableJSON(t *testing.T) {
|
||||
revisions := []*gitea.WikiCommit{{
|
||||
ID: "1234567890abcdef",
|
||||
Message: "second revision",
|
||||
Author: &gitea.CommitUser{GitIdentity: gitea.GitIdentity{Name: "bob"}, Date: "2026-05-22T03:04:05Z"},
|
||||
}}
|
||||
|
||||
tbl := wikiRevisionsTable(revisions, []string{"sha", "message", "author", "date"}, true)
|
||||
buf := &bytes.Buffer{}
|
||||
require.NoError(t, tbl.fprint(buf, "json"))
|
||||
|
||||
var rows []map[string]string
|
||||
require.NoError(t, json.Unmarshal(buf.Bytes(), &rows))
|
||||
require.Len(t, rows, 1)
|
||||
assert.Equal(t, "1234567", rows[0]["sha"])
|
||||
assert.Equal(t, "second revision", rows[0]["message"])
|
||||
assert.Equal(t, "bob", rows[0]["author"])
|
||||
assert.Equal(t, "2026-05-22T03:04:05Z", rows[0]["date"])
|
||||
}
|
||||
|
||||
func TestRenderWikiPageMarkdownReturnsDecodedWikiContent(t *testing.T) {
|
||||
page := &gitea.WikiPage{
|
||||
Title: "Home",
|
||||
HTMLURL: "https://gitea.example.com/octo/tea/wiki/Home",
|
||||
ContentBase64: "IyBIZWxsbyB3aWtp",
|
||||
LastCommit: &gitea.WikiCommit{
|
||||
Author: &gitea.CommitUser{GitIdentity: gitea.GitIdentity{Name: "carol"}, Date: "2026-05-23T06:07:08Z"},
|
||||
},
|
||||
}
|
||||
|
||||
out, err := renderWikiPageMarkdown(page)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "# Hello wiki\n", out)
|
||||
}
|
||||
|
||||
func TestWikiPagesListEmptyHumanOutput(t *testing.T) {
|
||||
out := captureStdout(t, func() {
|
||||
require.NoError(t, WikiPagesList(nil, "", []string{"title"}))
|
||||
})
|
||||
|
||||
assert.Equal(t, "No wiki pages found\n", out)
|
||||
}
|
||||
|
||||
func captureStdout(t *testing.T, fn func()) string {
|
||||
t.Helper()
|
||||
|
||||
oldStdout := os.Stdout
|
||||
reader, writer, err := os.Pipe()
|
||||
require.NoError(t, err)
|
||||
os.Stdout = writer
|
||||
defer func() {
|
||||
os.Stdout = oldStdout
|
||||
}()
|
||||
|
||||
done := make(chan string, 1)
|
||||
go func() {
|
||||
var outBuf bytes.Buffer
|
||||
_, _ = io.Copy(&outBuf, reader)
|
||||
done <- outBuf.String()
|
||||
}()
|
||||
|
||||
fn()
|
||||
require.NoError(t, writer.Close())
|
||||
output := <-done
|
||||
require.NoError(t, reader.Close())
|
||||
return output
|
||||
}
|
||||
Reference in new issue
Block a user