Add PR checks command, iostreams/text packages for colored table output, top-level run/workflow aliases matching gh CLI structure. Enhance actions, issues, PRs, releases, repos, labels, milestones, and wiki commands with improved flags, JSON output, and error handling.
405 lines
10 KiB
Go
405 lines
10 KiB
Go
package cmd
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"forgejo.zerova.net/sid/fgj-sid/internal/api"
|
|
"forgejo.zerova.net/sid/fgj-sid/internal/config"
|
|
"forgejo.zerova.net/sid/fgj-sid/internal/text"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// Wiki API response types
|
|
|
|
type wikiPageMeta struct {
|
|
Title string `json:"title"`
|
|
HTMLURL string `json:"html_url"`
|
|
SubURL string `json:"sub_url"`
|
|
LastCommit *wikiCommit `json:"last_commit"`
|
|
}
|
|
|
|
type wikiCommit struct {
|
|
ID string `json:"id"`
|
|
Author *wikiUser `json:"author"`
|
|
Committer *wikiUser `json:"committer"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type wikiUser struct {
|
|
Name string `json:"name"`
|
|
Email string `json:"email"`
|
|
Date string `json:"date"`
|
|
}
|
|
|
|
type wikiPage struct {
|
|
Title string `json:"title"`
|
|
HTMLURL string `json:"html_url"`
|
|
SubURL string `json:"sub_url"`
|
|
ContentBase64 string `json:"content_base64"`
|
|
LastCommit *wikiCommit `json:"last_commit"`
|
|
// Decoded content for JSON output
|
|
Content string `json:"content,omitempty"`
|
|
}
|
|
|
|
type wikiCreateRequest struct {
|
|
Title string `json:"title"`
|
|
ContentBase64 string `json:"content_base64"`
|
|
}
|
|
|
|
var wikiCmd = &cobra.Command{
|
|
Use: "wiki",
|
|
Short: "Manage repository wiki pages",
|
|
Long: "View and manage wiki pages for a repository.",
|
|
}
|
|
|
|
var wikiListCmd = &cobra.Command{
|
|
Use: "list",
|
|
Short: "List wiki pages",
|
|
Long: "List all wiki pages for a repository.",
|
|
Example: ` # List wiki pages for the current repo
|
|
fgj wiki list
|
|
|
|
# List wiki pages for a specific repo
|
|
fgj wiki list -R owner/repo
|
|
|
|
# Output as JSON
|
|
fgj wiki list --json`,
|
|
RunE: runWikiList,
|
|
}
|
|
|
|
var wikiViewCmd = &cobra.Command{
|
|
Use: "view <title>",
|
|
Short: "View a wiki page",
|
|
Long: "Display the content of a wiki page.",
|
|
Example: ` # View a wiki page
|
|
fgj wiki view Home
|
|
|
|
# Open in browser
|
|
fgj wiki view Home --web
|
|
|
|
# View a wiki page as JSON (includes content)
|
|
fgj wiki view Home --json
|
|
|
|
# View a wiki page from a specific repo
|
|
fgj wiki view "Getting-Started" -R owner/repo`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runWikiView,
|
|
}
|
|
|
|
var wikiCreateCmd = &cobra.Command{
|
|
Use: "create <title>",
|
|
Short: "Create a wiki page",
|
|
Long: "Create a new wiki page in the repository.",
|
|
Example: ` # Create a wiki page with inline content
|
|
fgj wiki create "Getting Started" -b "# Welcome\nThis is the getting started guide."
|
|
|
|
# Create a wiki page from a file
|
|
fgj wiki create "Setup Guide" --body-file setup.md
|
|
|
|
# Create a wiki page from stdin
|
|
echo "# FAQ" | fgj wiki create FAQ --body-file -
|
|
|
|
# Output as JSON
|
|
fgj wiki create "New Page" -b "Content here" --json`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runWikiCreate,
|
|
}
|
|
|
|
var wikiEditCmd = &cobra.Command{
|
|
Use: "edit <title>",
|
|
Short: "Edit a wiki page",
|
|
Long: "Edit an existing wiki page in the repository.",
|
|
Example: ` # Edit a wiki page with new content
|
|
fgj wiki edit Home -b "# Updated Home\nNew content here."
|
|
|
|
# Edit a wiki page from a file
|
|
fgj wiki edit "Setup Guide" --body-file updated-setup.md
|
|
|
|
# Edit a wiki page from stdin
|
|
cat new-content.md | fgj wiki edit Home --body-file -
|
|
|
|
# Output as JSON
|
|
fgj wiki edit Home -b "Updated content" --json`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runWikiEdit,
|
|
}
|
|
|
|
var wikiDeleteCmd = &cobra.Command{
|
|
Use: "delete <title>",
|
|
Short: "Delete a wiki page",
|
|
Long: "Delete a wiki page from the repository.",
|
|
Example: ` # Delete a wiki page
|
|
fgj wiki delete "Old Page"
|
|
|
|
# Delete without confirmation
|
|
fgj wiki delete "Old Page" -y
|
|
|
|
# Delete a wiki page from a specific repo
|
|
fgj wiki delete "Outdated Guide" -R owner/repo`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: runWikiDelete,
|
|
}
|
|
|
|
func init() {
|
|
rootCmd.AddCommand(wikiCmd)
|
|
wikiCmd.AddCommand(wikiListCmd)
|
|
wikiCmd.AddCommand(wikiViewCmd)
|
|
wikiCmd.AddCommand(wikiCreateCmd)
|
|
wikiCmd.AddCommand(wikiEditCmd)
|
|
wikiCmd.AddCommand(wikiDeleteCmd)
|
|
|
|
wikiListCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
|
addJSONFlags(wikiListCmd, "Output as JSON")
|
|
|
|
wikiViewCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
|
addJSONFlags(wikiViewCmd, "Output as JSON")
|
|
wikiViewCmd.Flags().BoolP("web", "w", false, "Open in web browser")
|
|
|
|
wikiCreateCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
|
wikiCreateCmd.Flags().StringP("body", "b", "", "Wiki page content")
|
|
wikiCreateCmd.Flags().String("body-file", "", "Read content from file (use \"-\" for stdin)")
|
|
addJSONFlags(wikiCreateCmd, "Output created page as JSON")
|
|
|
|
wikiEditCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
|
wikiEditCmd.Flags().StringP("body", "b", "", "Wiki page content")
|
|
wikiEditCmd.Flags().String("body-file", "", "Read content from file (use \"-\" for stdin)")
|
|
addJSONFlags(wikiEditCmd, "Output updated page as JSON")
|
|
|
|
wikiDeleteCmd.Flags().StringP("repo", "R", "", "Repository in owner/name format")
|
|
wikiDeleteCmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt")
|
|
}
|
|
|
|
func newWikiClient(cmd *cobra.Command) (*api.Client, string, string, error) {
|
|
repo, _ := cmd.Flags().GetString("repo")
|
|
owner, name, err := parseRepo(repo)
|
|
if err != nil {
|
|
return nil, "", "", err
|
|
}
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return nil, "", "", err
|
|
}
|
|
|
|
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost())
|
|
if err != nil {
|
|
return nil, "", "", err
|
|
}
|
|
|
|
return client, owner, name, nil
|
|
}
|
|
|
|
func runWikiList(cmd *cobra.Command, args []string) error {
|
|
client, owner, name, err := newWikiClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/pages", url.PathEscape(owner), url.PathEscape(name))
|
|
|
|
ios.StartSpinner("Fetching wiki pages...")
|
|
var pages []wikiPageMeta
|
|
if err := client.GetJSON(path, &pages); err != nil {
|
|
ios.StopSpinner()
|
|
return fmt.Errorf("failed to list wiki pages: %w", err)
|
|
}
|
|
ios.StopSpinner()
|
|
|
|
if wantJSON(cmd) {
|
|
return outputJSON(cmd, pages)
|
|
}
|
|
|
|
if len(pages) == 0 {
|
|
fmt.Fprintln(ios.Out, "No wiki pages found")
|
|
return nil
|
|
}
|
|
|
|
isTTY := ios.IsStdoutTTY()
|
|
tp := ios.NewTablePrinter()
|
|
tp.AddHeader("TITLE", "LAST UPDATED")
|
|
for _, p := range pages {
|
|
updated := ""
|
|
if p.LastCommit != nil && p.LastCommit.Committer != nil && p.LastCommit.Committer.Date != "" {
|
|
if t, err := time.Parse(time.RFC3339, p.LastCommit.Committer.Date); err == nil {
|
|
updated = text.FormatDate(t, isTTY)
|
|
} else {
|
|
updated = p.LastCommit.Committer.Date
|
|
}
|
|
}
|
|
tp.AddRow(p.Title, updated)
|
|
}
|
|
return tp.Render()
|
|
}
|
|
|
|
func runWikiView(cmd *cobra.Command, args []string) error {
|
|
title := args[0]
|
|
|
|
client, owner, name, err := newWikiClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s",
|
|
url.PathEscape(owner), url.PathEscape(name), url.PathEscape(title))
|
|
|
|
ios.StartSpinner("Fetching wiki page...")
|
|
var page wikiPage
|
|
if err := client.GetJSON(path, &page); err != nil {
|
|
ios.StopSpinner()
|
|
return fmt.Errorf("failed to get wiki page: %w", err)
|
|
}
|
|
ios.StopSpinner()
|
|
|
|
content, err := base64.StdEncoding.DecodeString(page.ContentBase64)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to decode wiki page content: %w", err)
|
|
}
|
|
|
|
if web, _ := cmd.Flags().GetBool("web"); web {
|
|
if page.HTMLURL != "" {
|
|
return ios.OpenInBrowser(page.HTMLURL)
|
|
}
|
|
return fmt.Errorf("wiki page has no HTML URL")
|
|
}
|
|
|
|
jsonFlag, _ := cmd.Flags().GetBool("json")
|
|
if jsonFlag {
|
|
page.Content = string(content)
|
|
return writeJSON(page)
|
|
}
|
|
|
|
if err := ios.StartPager(); err != nil {
|
|
fmt.Fprintf(ios.ErrOut, "warning: failed to start pager: %v\n", err)
|
|
}
|
|
defer ios.StopPager()
|
|
|
|
fmt.Fprintf(ios.Out, "# %s\n\n", page.Title)
|
|
fmt.Fprint(ios.Out, string(content))
|
|
// Ensure trailing newline
|
|
if len(content) > 0 && content[len(content)-1] != '\n' {
|
|
fmt.Fprintln(ios.Out)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func runWikiCreate(cmd *cobra.Command, args []string) error {
|
|
title := args[0]
|
|
|
|
client, owner, name, err := newWikiClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
body, err := readBody(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if body == "" {
|
|
return fmt.Errorf("content is required (use --body or --body-file)")
|
|
}
|
|
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/new",
|
|
url.PathEscape(owner), url.PathEscape(name))
|
|
|
|
reqBody := wikiCreateRequest{
|
|
Title: title,
|
|
ContentBase64: base64.StdEncoding.EncodeToString([]byte(body)),
|
|
}
|
|
|
|
ios.StartSpinner("Creating wiki page...")
|
|
var page wikiPage
|
|
if _, err := client.DoJSON(http.MethodPost, path, reqBody, &page); err != nil {
|
|
ios.StopSpinner()
|
|
return fmt.Errorf("failed to create wiki page: %w", err)
|
|
}
|
|
ios.StopSpinner()
|
|
|
|
if wantJSON(cmd) {
|
|
return outputJSON(cmd, page)
|
|
}
|
|
|
|
cs := ios.ColorScheme()
|
|
fmt.Fprintf(ios.Out, "%s Wiki page created: %s\n", cs.SuccessIcon(), title)
|
|
return nil
|
|
}
|
|
|
|
func runWikiEdit(cmd *cobra.Command, args []string) error {
|
|
title := args[0]
|
|
|
|
client, owner, name, err := newWikiClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
body, err := readBody(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if body == "" {
|
|
return fmt.Errorf("content is required (use --body or --body-file)")
|
|
}
|
|
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s",
|
|
url.PathEscape(owner), url.PathEscape(name), url.PathEscape(title))
|
|
|
|
reqBody := wikiCreateRequest{
|
|
Title: title,
|
|
ContentBase64: base64.StdEncoding.EncodeToString([]byte(body)),
|
|
}
|
|
|
|
ios.StartSpinner("Updating wiki page...")
|
|
var page wikiPage
|
|
if _, err := client.DoJSON(http.MethodPatch, path, reqBody, &page); err != nil {
|
|
ios.StopSpinner()
|
|
return fmt.Errorf("failed to update wiki page: %w", err)
|
|
}
|
|
ios.StopSpinner()
|
|
|
|
if wantJSON(cmd) {
|
|
return outputJSON(cmd, page)
|
|
}
|
|
|
|
cs := ios.ColorScheme()
|
|
fmt.Fprintf(ios.Out, "%s Wiki page updated: %s\n", cs.SuccessIcon(), title)
|
|
return nil
|
|
}
|
|
|
|
func runWikiDelete(cmd *cobra.Command, args []string) error {
|
|
title := args[0]
|
|
yes, _ := cmd.Flags().GetBool("yes")
|
|
|
|
client, owner, name, err := newWikiClient(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !yes {
|
|
confirmed, err := ios.ConfirmAction(fmt.Sprintf("Delete wiki page %q?", title))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !confirmed {
|
|
fmt.Fprintln(ios.ErrOut, "Aborted")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/wiki/page/%s",
|
|
url.PathEscape(owner), url.PathEscape(name), url.PathEscape(title))
|
|
|
|
ios.StartSpinner("Deleting wiki page...")
|
|
if _, err := client.DoJSON(http.MethodDelete, path, nil, nil); err != nil {
|
|
ios.StopSpinner()
|
|
return fmt.Errorf("failed to delete wiki page: %w", err)
|
|
}
|
|
ios.StopSpinner()
|
|
|
|
cs := ios.ColorScheme()
|
|
fmt.Fprintf(ios.Out, "%s Wiki page deleted: %s\n", cs.SuccessIcon(), title)
|
|
return nil
|
|
}
|