mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
189 lines
7.0 KiB
Go
189 lines
7.0 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package services
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
gitea "gitea.dev/sdk"
|
|
"gitea.dev/tea/internal/transport"
|
|
)
|
|
|
|
// WikiPage exposes decoded page content and revision metadata.
|
|
type WikiPage struct {
|
|
Title string `json:"title"`
|
|
Body string `json:"body"`
|
|
URL string `json:"url"`
|
|
SHA string `json:"sha"`
|
|
Author string `json:"author"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
CommitCount int64 `json:"commit_count"`
|
|
Sidebar string `json:"sidebar"`
|
|
Footer string `json:"footer"`
|
|
}
|
|
|
|
// WikiRevision represents a wiki commit without exposing author email addresses.
|
|
type WikiRevision struct {
|
|
SHA string `json:"sha"`
|
|
Message string `json:"message"`
|
|
Author string `json:"author"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
func wikiRevisionFrom(commit *gitea.WikiCommit) WikiRevision {
|
|
result := WikiRevision{SHA: commit.ID, Message: commit.Message}
|
|
if commit.Author != nil {
|
|
result.Author = commit.Author.Name
|
|
result.CreatedAt = commit.Author.Date
|
|
}
|
|
return result
|
|
}
|
|
|
|
func wikiPageFrom(title, url string, lastCommit *gitea.WikiCommit) WikiPage {
|
|
result := WikiPage{Title: title, URL: url}
|
|
if lastCommit != nil {
|
|
revision := wikiRevisionFrom(lastCommit)
|
|
result.SHA, result.Author, result.UpdatedAt = revision.SHA, revision.Author, revision.CreatedAt
|
|
}
|
|
return result
|
|
}
|
|
|
|
func wikiContentFrom(page *gitea.WikiPage) (WikiPage, error) {
|
|
body, err := base64.StdEncoding.DecodeString(page.ContentBase64)
|
|
if err != nil {
|
|
return WikiPage{}, fmt.Errorf("decode wiki content: %w", err)
|
|
}
|
|
result := wikiPageFrom(page.Title, page.HTMLURL, page.LastCommit)
|
|
result.Body, result.CommitCount, result.Sidebar, result.Footer = string(body), page.CommitCount, page.Sidebar, page.Footer
|
|
return result, nil
|
|
}
|
|
|
|
// WikiOptions supplies title, decoded content and a commit message.
|
|
type WikiOptions struct{ Title, Body, Message string }
|
|
|
|
// Wiki implements repository wiki operations.
|
|
type Wiki struct {
|
|
Client *transport.Client
|
|
Repository string
|
|
}
|
|
|
|
func (s Wiki) page(ctx context.Context, op repoOperation[*gitea.WikiPage]) (WikiPage, error) {
|
|
page, err := repoCall(ctx, s.Client, s.Repository, op)
|
|
if err != nil {
|
|
return WikiPage{}, err
|
|
}
|
|
return wikiContentFrom(page)
|
|
}
|
|
|
|
// wikiPageSize is the page size for reading the whole wiki; Gitea replaces a limit of 1 with its
|
|
// default page size.
|
|
const wikiPageSize = 50
|
|
|
|
// ListPage lists a page of wiki pages, with whether more follow and, when the whole wiki was read,
|
|
// how many there are. Gitea windows the wiki's files, not its pages: a window holds fewer pages
|
|
// than its size, or none, while later windows still hold pages. The windows are read in order
|
|
// while the server links a next one, until one page past the requested page is found.
|
|
func (s Wiki) ListPage(ctx context.Context, limit, page int) (Page[WikiPage], error) {
|
|
if limit < 1 || page < 1 {
|
|
return Page[WikiPage]{}, fmt.Errorf("limit and page must be positive")
|
|
}
|
|
all, wanted := []WikiPage{}, page*limit
|
|
for window := 1; ; window++ {
|
|
if window > maxListPages {
|
|
return Page[WikiPage]{}, fmt.Errorf("listing exceeded %d pages", maxListPages)
|
|
}
|
|
var headers http.Header
|
|
items, err := repoCall(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, repo string) ([]*gitea.WikiPageMetaData, *gitea.Response, error) {
|
|
items, resp, err := c.Wiki.ListPages(ctx, owner, repo, gitea.ListWikiPagesOptions{ListOptions: listOptions(wikiPageSize, window)})
|
|
headers = headerOf(resp)
|
|
return items, resp, err
|
|
})
|
|
if err != nil {
|
|
if window > 1 || !IsNotFound(err) {
|
|
return Page[WikiPage]{}, err
|
|
}
|
|
// An uninitialized wiki returns 404. Verify repository visibility and
|
|
// wiki configuration before treating that response as an empty list.
|
|
repository, checkErr := (Repositories{Client: s.Client}).get(ctx, s.Repository)
|
|
if checkErr != nil {
|
|
return Page[WikiPage]{}, checkErr
|
|
}
|
|
if !repository.HasWiki {
|
|
return Page[WikiPage]{}, err
|
|
}
|
|
return Page[WikiPage]{Items: []WikiPage{}, Counted: true}, nil
|
|
}
|
|
all = append(all, convertAll(items, func(item *gitea.WikiPageMetaData) WikiPage {
|
|
return wikiPageFrom(item.Title, item.HTMLURL, item.LastCommit)
|
|
})...)
|
|
if next, _, _ := pageHints(headers); !next {
|
|
break
|
|
}
|
|
if len(all) >= wanted {
|
|
items, err := pageOf(all, limit, page)
|
|
return Page[WikiPage]{Items: items, More: true}, err
|
|
}
|
|
}
|
|
items, err := pageOf(all, limit, page)
|
|
return Page[WikiPage]{Items: items, More: page*limit < len(all), Total: len(all), Counted: true}, err
|
|
}
|
|
|
|
// View retrieves and decodes a wiki page.
|
|
func (s Wiki) View(ctx context.Context, name string) (WikiPage, error) {
|
|
item, err := s.page(ctx, func(ctx context.Context, c *gitea.Client, owner, repo string) (*gitea.WikiPage, *gitea.Response, error) {
|
|
return c.Wiki.GetPage(ctx, owner, repo, name)
|
|
})
|
|
return item, Classify(err, "wiki page", strconv.Quote(name), s.Repository)
|
|
}
|
|
|
|
func wikiOptions(opts WikiOptions) gitea.CreateWikiPageOptions {
|
|
return gitea.CreateWikiPageOptions{Title: opts.Title, ContentBase64: base64.StdEncoding.EncodeToString([]byte(opts.Body)), Message: opts.Message}
|
|
}
|
|
|
|
// Create creates one wiki page, initializing the wiki if necessary.
|
|
func (s Wiki) Create(ctx context.Context, opts WikiOptions) (WikiPage, error) {
|
|
item, err := s.page(ctx, func(ctx context.Context, c *gitea.Client, owner, repo string) (*gitea.WikiPage, *gitea.Response, error) {
|
|
return c.Wiki.CreatePage(ctx, owner, repo, wikiOptions(opts))
|
|
})
|
|
return item, Classify(err, "wiki page", strconv.Quote(opts.Title), s.Repository)
|
|
}
|
|
|
|
// Edit replaces page content and optionally changes its title.
|
|
func (s Wiki) Edit(ctx context.Context, name string, opts WikiOptions) (WikiPage, error) {
|
|
if opts.Title == "" {
|
|
current, err := s.View(ctx, name)
|
|
if err != nil {
|
|
return WikiPage{}, err
|
|
}
|
|
opts.Title = current.Title
|
|
}
|
|
item, err := s.page(ctx, func(ctx context.Context, c *gitea.Client, owner, repo string) (*gitea.WikiPage, *gitea.Response, error) {
|
|
return c.Wiki.EditPage(ctx, owner, repo, name, wikiOptions(opts))
|
|
})
|
|
return item, Classify(err, "wiki page", strconv.Quote(name), s.Repository)
|
|
}
|
|
|
|
// Delete removes a named wiki page.
|
|
func (s Wiki) Delete(ctx context.Context, name string) error {
|
|
err := repoDo(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, repo string) (*gitea.Response, error) {
|
|
return c.Wiki.DeletePage(ctx, owner, repo, name)
|
|
})
|
|
return Classify(err, "wiki page", strconv.Quote(name), s.Repository)
|
|
}
|
|
|
|
// Revisions returns one server-paginated page of wiki commits.
|
|
func (s Wiki) Revisions(ctx context.Context, name string, page int) ([]WikiRevision, error) {
|
|
revisions, err := repoCall(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, repo string) (*gitea.WikiCommitList, *gitea.Response, error) {
|
|
return c.Wiki.GetPageRevisions(ctx, owner, repo, name, gitea.ListWikiPageRevisionsOptions{Page: page})
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return convertAll(revisions.WikiCommits, wikiRevisionFrom), nil
|
|
}
|