mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
webhook create --events pushh created a webhook that fired on nothing, since Gitea drops unknown events, and unknown types or a Slack webhook without a channel failed with raw HTTP 422 errors. --events and --type now accept only what Gitea knows, list it in help and complete it, and a Slack webhook needs --config channel=. Events came back in a different order on every call; they are sorted now. Webhooks can be told apart again: list, view and JSON show the delivery URL without user information and query, and without the path for chat services whose paths carry tokens; view --reveal-url shows it whole. Tables collapse complete event groups such as pull_request (8). On a terminal, webhook edit no longer needs an authorization flag: since Gitea drops the stored header on every edit and never returns it, it asks for the header again and keeps it, and an empty answer suits webhooks without one. Without a terminal the edit still requires the flags. Edit flags lose the create defaults, the unsupported secret flags are hidden on edit, deletions name the webhook and its scope, and mutations print ✓ lines. terminal.View gains Bodyless for resources without a body. Signed-off-by: bircni <bircni@icloud.com>
135 lines
3.8 KiB
Go
135 lines
3.8 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package terminal
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/mattn/go-runewidth"
|
|
)
|
|
|
|
// Field is one labeled fact in a View.
|
|
type Field struct {
|
|
Label string
|
|
// Value is plain text. Cells, when set, replace it with styled values joined by commas, such
|
|
// as colored labels.
|
|
Value string
|
|
Cells []Cell
|
|
// TTYOnly and PipedOnly restrict the field to one kind of output, like table columns.
|
|
TTYOnly, PipedOnly bool
|
|
}
|
|
|
|
// text returns the field's safe value, styled for d when it is made of cells.
|
|
func (f Field) text(d Display) string {
|
|
if len(f.Cells) == 0 {
|
|
return SafeLine(f.Value)
|
|
}
|
|
values := make([]string, len(f.Cells))
|
|
for i, cell := range f.Cells {
|
|
values[i] = SafeLine(cell.Text)
|
|
if d.TTY {
|
|
values[i] = cell.style(values[i], d, false)
|
|
}
|
|
}
|
|
return strings.Join(values, ", ")
|
|
}
|
|
|
|
// View is one resource for human readers, such as an issue, a release or a repository.
|
|
//
|
|
// On a TTY it shows the bold title with its muted ID, a line of badges and muted metadata, the
|
|
// aligned non-empty fields, the body (markdown rendered when the display allows), and the footer
|
|
// "View this <kind> on Gitea: <url>". Otherwise it writes "key:\tvalue" lines for the title, the
|
|
// fields and the URL, then "--" and the raw body, for scripts.
|
|
type View struct {
|
|
Title, ID string
|
|
// Badges, such as the state, start the line under the title; Meta completes it, for example
|
|
// "alice opened about 2 hours ago • 2 comments".
|
|
Badges []Cell
|
|
Meta string
|
|
Fields []Field
|
|
Body string
|
|
// Bodyless resources, such as webhooks, have no body: the view ends with the fields and URL.
|
|
Bodyless bool
|
|
// Kind names the resource in the footer, such as "issue".
|
|
Kind, URL string
|
|
}
|
|
|
|
// Render formats the view for d. All server text is sanitized.
|
|
func (v View) Render(d Display) string {
|
|
var b strings.Builder
|
|
if !d.TTY {
|
|
if v.Title != "" {
|
|
b.WriteString("title:\t" + SafeLine(v.Title) + "\n")
|
|
}
|
|
for _, field := range v.Fields {
|
|
if !field.TTYOnly {
|
|
b.WriteString(strings.ToLower(SafeLine(field.Label)) + ":\t" + field.text(d) + "\n")
|
|
}
|
|
}
|
|
if v.URL != "" {
|
|
b.WriteString("url:\t" + SafeLine(v.URL) + "\n")
|
|
}
|
|
if v.Bodyless {
|
|
return b.String()
|
|
}
|
|
body := SafeText(v.Body)
|
|
b.WriteString("--\n" + body)
|
|
if !strings.HasSuffix(body, "\n") {
|
|
b.WriteByte('\n')
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
muted := func(text string) string { return Cell{Muted: true}.style(text, d, false) }
|
|
b.WriteString(Cell{Bold: true}.style(SafeLine(v.Title), d, false))
|
|
if v.ID != "" {
|
|
b.WriteString(" " + muted(SafeLine(v.ID)))
|
|
}
|
|
b.WriteByte('\n')
|
|
var summary []string
|
|
for _, badge := range v.Badges {
|
|
summary = append(summary, badge.style(SafeLine(badge.Text), d, false))
|
|
}
|
|
if meta := SafeLine(v.Meta); meta != "" {
|
|
summary = append(summary, muted(meta))
|
|
}
|
|
if len(summary) > 0 {
|
|
b.WriteString(strings.Join(summary, " • ") + "\n")
|
|
}
|
|
|
|
var fields []Field
|
|
labelWidth := 0
|
|
for _, field := range v.Fields {
|
|
if !field.PipedOnly && (field.Value != "" || len(field.Cells) > 0) {
|
|
fields = append(fields, field)
|
|
labelWidth = max(labelWidth, runewidth.StringWidth(SafeLine(field.Label)))
|
|
}
|
|
}
|
|
for _, field := range fields {
|
|
label := SafeLine(field.Label) + ":"
|
|
b.WriteString(Cell{Bold: true}.style(label, d, false) + strings.Repeat(" ", labelWidth+2-runewidth.StringWidth(label)) + field.text(d) + "\n")
|
|
}
|
|
|
|
if !v.Bodyless {
|
|
body := strings.TrimSpace(v.Body)
|
|
switch {
|
|
case body == "":
|
|
body = muted("No description provided")
|
|
case d.Markdown:
|
|
body = Markdown(body, d)
|
|
default:
|
|
body = SafeText(body)
|
|
}
|
|
b.WriteString("\n" + body + "\n")
|
|
}
|
|
if v.URL != "" {
|
|
kind := "View on Gitea: "
|
|
if v.Kind != "" {
|
|
kind = "View this " + v.Kind + " on Gitea: "
|
|
}
|
|
b.WriteString("\n" + muted(kind+SafeLine(v.URL)) + "\n")
|
|
}
|
|
return b.String()
|
|
}
|