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:
bircni committed 2026-10-11 17:23:06 +00:00
1 parent 12e415a7a7
commit f02245449b
456 files changed
+18892 -34015

No files matched your search

-443
View File
@@ -1,443 +0,0 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package webhooks
import (
"testing"
"gitea.dev/sdk"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v3"
)
func TestDeleteCommandMetadata(t *testing.T) {
cmd := &CmdWebhooksDelete
assert.Equal(t, "delete", cmd.Name)
assert.Contains(t, cmd.Aliases, "rm")
assert.Equal(t, "Delete a webhook", cmd.Usage)
assert.Equal(t, "Delete a webhook by ID from repository, organization, or globally", cmd.Description)
assert.Equal(t, "<webhook-id>", cmd.ArgsUsage)
assert.NotNil(t, cmd.Action)
}
func TestDeleteCommandFlags(t *testing.T) {
cmd := &CmdWebhooksDelete
expectedFlags := []string{
"confirm",
}
for _, flagName := range expectedFlags {
found := false
for _, flag := range cmd.Flags {
if flag.Names()[0] == flagName {
found = true
break
}
}
assert.True(t, found, "Expected flag %s not found", flagName)
}
// Check that confirm flag has correct aliases
for _, flag := range cmd.Flags {
if flag.Names()[0] == "confirm" {
if boolFlag, ok := flag.(*cli.BoolFlag); ok {
assert.Contains(t, boolFlag.Aliases, "y")
}
}
}
}
func TestDeleteConfirmationLogic(t *testing.T) {
tests := []struct {
name string
confirmFlag bool
userResponse string
shouldDelete bool
shouldPrompt bool
}{
{
name: "Confirm flag set - should delete",
confirmFlag: true,
userResponse: "",
shouldDelete: true,
shouldPrompt: false,
},
{
name: "No confirm flag, user says yes",
confirmFlag: false,
userResponse: "y",
shouldDelete: true,
shouldPrompt: true,
},
{
name: "No confirm flag, user says Yes",
confirmFlag: false,
userResponse: "Y",
shouldDelete: true,
shouldPrompt: true,
},
{
name: "No confirm flag, user says yes (full)",
confirmFlag: false,
userResponse: "yes",
shouldDelete: true,
shouldPrompt: true,
},
{
name: "No confirm flag, user says no",
confirmFlag: false,
userResponse: "n",
shouldDelete: false,
shouldPrompt: true,
},
{
name: "No confirm flag, user says No",
confirmFlag: false,
userResponse: "N",
shouldDelete: false,
shouldPrompt: true,
},
{
name: "No confirm flag, user says no (full)",
confirmFlag: false,
userResponse: "no",
shouldDelete: false,
shouldPrompt: true,
},
{
name: "No confirm flag, empty response",
confirmFlag: false,
userResponse: "",
shouldDelete: false,
shouldPrompt: true,
},
{
name: "No confirm flag, invalid response",
confirmFlag: false,
userResponse: "maybe",
shouldDelete: false,
shouldPrompt: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Simulate the confirmation logic from runWebhooksDelete
shouldDelete := tt.confirmFlag
shouldPrompt := !tt.confirmFlag
if !tt.confirmFlag {
response := tt.userResponse
shouldDelete = response == "y" || response == "Y" || response == "yes"
}
assert.Equal(t, tt.shouldDelete, shouldDelete, "Delete decision mismatch")
assert.Equal(t, tt.shouldPrompt, shouldPrompt, "Prompt decision mismatch")
})
}
}
func TestDeleteWebhookIDValidation(t *testing.T) {
tests := []struct {
name string
webhookID string
expectedID int64
expectError bool
}{
{
name: "Valid webhook ID",
webhookID: "123",
expectedID: 123,
expectError: false,
},
{
name: "Single digit ID",
webhookID: "1",
expectedID: 1,
expectError: false,
},
{
name: "Large webhook ID",
webhookID: "999999",
expectedID: 999999,
expectError: false,
},
{
name: "Zero webhook ID",
webhookID: "0",
expectedID: 0,
expectError: true,
},
{
name: "Negative webhook ID",
webhookID: "-1",
expectedID: 0,
expectError: true,
},
{
name: "Non-numeric webhook ID",
webhookID: "abc",
expectedID: 0,
expectError: true,
},
{
name: "Empty webhook ID",
webhookID: "",
expectedID: 0,
expectError: true,
},
{
name: "Float webhook ID",
webhookID: "12.34",
expectedID: 0,
expectError: true,
},
{
name: "Webhook ID with spaces",
webhookID: " 123 ",
expectedID: 0,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// This simulates the utils.ArgToIndex function behavior
if tt.webhookID == "" {
assert.True(t, tt.expectError)
return
}
// Basic validation - check if it's numeric and positive
isValid := true
if len(tt.webhookID) == 0 {
isValid = false
} else {
for _, char := range tt.webhookID {
if char < '0' || char > '9' {
isValid = false
break
}
}
// Check for zero or negative
if isValid && (tt.webhookID == "0" || (len(tt.webhookID) > 0 && tt.webhookID[0] == '-')) {
isValid = false
}
}
if !isValid {
assert.True(t, tt.expectError, "Should expect error for invalid ID: %s", tt.webhookID)
} else {
assert.False(t, tt.expectError, "Should not expect error for valid ID: %s", tt.webhookID)
}
})
}
}
func TestDeletePromptMessage(t *testing.T) {
// Test that the prompt message includes webhook information
webhook := &gitea.Hook{
ID: 123,
Config: map[string]string{
"url": "https://example.com/webhook",
},
}
expectedElements := []string{
"123", // webhook ID
"https://example.com/webhook", // webhook URL
"Are you sure", // confirmation prompt
"[y/N]", // yes/no options with default No
}
// Simulate the prompt message format using webhook data
promptMessage := "Are you sure you want to delete webhook " + string(rune(webhook.ID+'0')) + " (" + webhook.Config["url"] + ")? [y/N] "
// For testing purposes, use the expected format
if webhook.ID > 9 {
promptMessage = "Are you sure you want to delete webhook 123 (https://example.com/webhook)? [y/N] "
}
for _, element := range expectedElements {
assert.Contains(t, promptMessage, element, "Prompt should contain %s", element)
}
}
func TestDeleteWebhookConfigAccess(t *testing.T) {
tests := []struct {
name string
webhook *gitea.Hook
expectedURL string
}{
{
name: "Webhook with URL in config",
webhook: &gitea.Hook{
ID: 123,
Config: map[string]string{
"url": "https://example.com/webhook",
},
},
expectedURL: "https://example.com/webhook",
},
{
name: "Webhook with nil config",
webhook: &gitea.Hook{
ID: 456,
Config: nil,
},
expectedURL: "",
},
{
name: "Webhook with empty config",
webhook: &gitea.Hook{
ID: 789,
Config: map[string]string{},
},
expectedURL: "",
},
{
name: "Webhook config without URL",
webhook: &gitea.Hook{
ID: 999,
Config: map[string]string{
"secret": "my-secret",
},
},
expectedURL: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var url string
if tt.webhook.Config != nil {
url = tt.webhook.Config["url"]
}
assert.Equal(t, tt.expectedURL, url)
})
}
}
func TestDeleteErrorHandling(t *testing.T) {
// Test various error conditions that delete command should handle
errorScenarios := []struct {
name string
description string
critical bool
}{
{
name: "Webhook not found",
description: "Should handle 404 errors gracefully",
critical: false,
},
{
name: "Permission denied",
description: "Should handle 403 errors gracefully",
critical: false,
},
{
name: "Network error",
description: "Should handle network connectivity issues",
critical: false,
},
{
name: "Authentication failure",
description: "Should handle authentication errors",
critical: false,
},
{
name: "Server error",
description: "Should handle 500 errors gracefully",
critical: false,
},
{
name: "Missing webhook ID",
description: "Should require webhook ID argument",
critical: true,
},
{
name: "Invalid webhook ID format",
description: "Should validate webhook ID format",
critical: true,
},
}
for _, scenario := range errorScenarios {
t.Run(scenario.name, func(t *testing.T) {
assert.NotEmpty(t, scenario.description)
// Critical errors should be caught before API calls
// Non-critical errors should be handled gracefully
})
}
}
func TestDeleteFlagConfiguration(t *testing.T) {
cmd := &CmdWebhooksDelete
// Test confirm flag configuration
var confirmFlag *cli.BoolFlag
for _, flag := range cmd.Flags {
if flag.Names()[0] == "confirm" {
if boolFlag, ok := flag.(*cli.BoolFlag); ok {
confirmFlag = boolFlag
break
}
}
}
assert.NotNil(t, confirmFlag, "Confirm flag should exist")
assert.Equal(t, "confirm", confirmFlag.Name)
assert.Contains(t, confirmFlag.Aliases, "y")
assert.Equal(t, "confirm deletion without prompting", confirmFlag.Usage)
}
func TestDeleteSuccessMessage(t *testing.T) {
tests := []struct {
name string
webhookID int64
expected string
}{
{
name: "Single digit ID",
webhookID: 1,
expected: "Webhook 1 deleted successfully\n",
},
{
name: "Multi digit ID",
webhookID: 123,
expected: "Webhook 123 deleted successfully\n",
},
{
name: "Large ID",
webhookID: 999999,
expected: "Webhook 999999 deleted successfully\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Simulate the success message format
message := "Webhook " + string(rune(tt.webhookID+'0')) + " deleted successfully\n"
// For multi-digit numbers, we need proper string conversion
if tt.webhookID > 9 {
// This is a simplified test - in real code, strconv.FormatInt would be used
assert.Contains(t, tt.expected, "deleted successfully")
} else {
assert.Contains(t, message, "deleted successfully")
}
})
}
}
func TestDeleteCancellationMessage(t *testing.T) {
expectedMessage := "Deletion canceled."
assert.NotEmpty(t, expectedMessage)
assert.Contains(t, expectedMessage, "canceled")
assert.NotContains(t, expectedMessage, "\n", "Cancellation message should not end with newline")
}