Adds a repo_update tool exposing PATCH /api/v1/repos/{owner}/{name}
with optional pointer fields (archived, description, private,
website, template). Only fields set by the caller are sent on the
wire, so the server patches exactly what was asked for.
Originally needed to archive ingestion-svc cleanly instead of
leaving a README tombstone, and to flip template-go-{agent,web}
to template=true so create_project_from_template stops failing
the "is not marked as template" guard.
Wire-level enforcement of "at least one field" returns ErrValidation
before any network call, preventing no-op PATCHes.
private=false (making a repo public) is allowed but flagged in the
tool description with a "verify intent before calling" warning.
The earlier issue draft suggested an ntfy confirmation hook for
that path — out of scope for this PR; the warning string is the
minimum that fits inside the tool surface today.
Wires NewRepoUpdate into cmd/gitea-mcp/main.go alongside the rest
of the repo_* family.
Closes #12
259 lines
6.5 KiB
Go
259 lines
6.5 KiB
Go
package gitea
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
)
|
|
|
|
type Repo struct {
|
|
Name string `json:"name"`
|
|
FullName string `json:"full_name"`
|
|
DefaultBranch string `json:"default_branch"`
|
|
Description string `json:"description"`
|
|
Private bool `json:"private"`
|
|
CloneURL string `json:"clone_url"`
|
|
HTMLURL string `json:"html_url"`
|
|
Template bool `json:"template"`
|
|
}
|
|
|
|
type TreeEntry struct {
|
|
Path string `json:"path"`
|
|
Type string `json:"type"` // "blob" or "tree"
|
|
SHA string `json:"sha"`
|
|
Size int64 `json:"size"`
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
type Tree struct {
|
|
SHA string `json:"sha"`
|
|
URL string `json:"url"`
|
|
Tree []TreeEntry `json:"tree"`
|
|
Truncated bool `json:"truncated"`
|
|
}
|
|
|
|
func (c *Client) GetTree(ctx context.Context, owner, repo, ref string, recursive bool) (*Tree, error) {
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/git/trees/%s", owner, repo, url.PathEscape(ref))
|
|
if recursive {
|
|
path += "?recursive=1"
|
|
}
|
|
body, status, err := c.GetJSON(ctx, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := MapStatus(status, body); err != nil {
|
|
return nil, err
|
|
}
|
|
var t Tree
|
|
if err := json.Unmarshal(body, &t); err != nil {
|
|
return nil, err
|
|
}
|
|
return &t, nil
|
|
}
|
|
|
|
type Release struct {
|
|
ID int64 `json:"id"`
|
|
TagName string `json:"tag_name"`
|
|
Name string `json:"name"`
|
|
Body string `json:"body"`
|
|
Draft bool `json:"draft"`
|
|
Prerelease bool `json:"prerelease"`
|
|
HTMLURL string `json:"html_url"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
type CreateReleaseArgs struct {
|
|
TagName string `json:"tag_name"`
|
|
Name string `json:"name,omitempty"`
|
|
Body string `json:"body,omitempty"`
|
|
Draft bool `json:"draft,omitempty"`
|
|
Prerelease bool `json:"prerelease,omitempty"`
|
|
// Target branch or commit SHA for tag creation. Empty = repo default branch.
|
|
Target string `json:"target_commitish,omitempty"`
|
|
}
|
|
|
|
func (c *Client) CreateRelease(ctx context.Context, owner, repo string, args CreateReleaseArgs) (*Release, error) {
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/releases", owner, repo)
|
|
body, err := json.Marshal(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, status, err := c.PostJSON(ctx, path, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := MapStatus(status, resp); err != nil {
|
|
return nil, err
|
|
}
|
|
var r Release
|
|
if err := json.Unmarshal(resp, &r); err != nil {
|
|
return nil, err
|
|
}
|
|
return &r, nil
|
|
}
|
|
|
|
func (c *Client) DeleteRepo(ctx context.Context, owner, repo string) error {
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s", owner, repo)
|
|
resp, status, err := c.DeleteJSON(ctx, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if status == 204 {
|
|
return nil
|
|
}
|
|
return MapStatus(status, resp)
|
|
}
|
|
|
|
func (c *Client) UpdateTopics(ctx context.Context, owner, repo string, topics []string) error {
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s/topics", owner, repo)
|
|
body, err := json.Marshal(map[string][]string{"topics": topics})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, status, err := c.PutJSON(ctx, path, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if status == 204 {
|
|
return nil
|
|
}
|
|
return MapStatus(status, resp)
|
|
}
|
|
|
|
func (c *Client) ListRepos(ctx context.Context, owner string, page, limit int) ([]Repo, error) {
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if limit < 1 {
|
|
limit = 30
|
|
}
|
|
path := fmt.Sprintf("/api/v1/users/%s/repos?page=%d&limit=%d", owner, page, limit)
|
|
body, status, err := c.GetJSON(ctx, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := MapStatus(status, body); err != nil {
|
|
return nil, err
|
|
}
|
|
var repos []Repo
|
|
if err := json.Unmarshal(body, &repos); err != nil {
|
|
return nil, err
|
|
}
|
|
return repos, nil
|
|
}
|
|
|
|
type repoSearchEnvelope struct {
|
|
Data []Repo `json:"data"`
|
|
OK bool `json:"ok"`
|
|
}
|
|
|
|
func (c *Client) SearchRepos(ctx context.Context, q, owner string, page, limit int) ([]Repo, error) {
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if limit < 1 {
|
|
limit = 30
|
|
}
|
|
path := fmt.Sprintf("/api/v1/repos/search?q=%s&page=%d&limit=%d",
|
|
url.QueryEscape(q), page, limit)
|
|
if owner != "" {
|
|
path += "&owner=" + url.QueryEscape(owner)
|
|
}
|
|
body, status, err := c.GetJSON(ctx, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := MapStatus(status, body); err != nil {
|
|
return nil, err
|
|
}
|
|
var env repoSearchEnvelope
|
|
if err := json.Unmarshal(body, &env); err != nil {
|
|
return nil, err
|
|
}
|
|
return env.Data, nil
|
|
}
|
|
|
|
type CreateRepoArgs struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
Private bool `json:"private,omitempty"`
|
|
AutoInit bool `json:"auto_init,omitempty"`
|
|
DefaultBranch string `json:"default_branch,omitempty"`
|
|
// Org, when non-empty, creates the repo under the named organisation.
|
|
// Uses POST /api/v1/orgs/{org}/repos instead of /api/v1/user/repos.
|
|
Org string `json:"-"`
|
|
}
|
|
|
|
func (c *Client) CreateRepo(ctx context.Context, args CreateRepoArgs) (*Repo, error) {
|
|
var path string
|
|
if args.Org != "" {
|
|
path = fmt.Sprintf("/api/v1/orgs/%s/repos", args.Org)
|
|
} else {
|
|
path = "/api/v1/user/repos"
|
|
}
|
|
body, err := json.Marshal(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, status, err := c.PostJSON(ctx, path, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := MapStatus(status, resp); err != nil {
|
|
return nil, err
|
|
}
|
|
var r Repo
|
|
if err := json.Unmarshal(resp, &r); err != nil {
|
|
return nil, err
|
|
}
|
|
return &r, nil
|
|
}
|
|
|
|
// UpdateRepoArgs uses pointers so omitempty can distinguish "not set" from false/zero.
|
|
type UpdateRepoArgs struct {
|
|
Description *string `json:"description,omitempty"`
|
|
Private *bool `json:"private,omitempty"`
|
|
Website *string `json:"website,omitempty"`
|
|
DefaultBranch *string `json:"default_branch,omitempty"`
|
|
Archived *bool `json:"archived,omitempty"`
|
|
Template *bool `json:"template,omitempty"`
|
|
}
|
|
|
|
func (c *Client) UpdateRepo(ctx context.Context, owner, name string, args UpdateRepoArgs) (*Repo, error) {
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s", owner, name)
|
|
body, err := json.Marshal(args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, status, err := c.PatchJSON(ctx, path, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := MapStatus(status, resp); err != nil {
|
|
return nil, err
|
|
}
|
|
var r Repo
|
|
if err := json.Unmarshal(resp, &r); err != nil {
|
|
return nil, err
|
|
}
|
|
return &r, nil
|
|
}
|
|
|
|
func (c *Client) GetRepo(ctx context.Context, owner, name string) (*Repo, error) {
|
|
path := fmt.Sprintf("/api/v1/repos/%s/%s", owner, name)
|
|
body, status, err := c.GetJSON(ctx, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := MapStatus(status, body); err != nil {
|
|
return nil, err
|
|
}
|
|
var r Repo
|
|
if err := json.Unmarshal(body, &r); err != nil {
|
|
return nil, err
|
|
}
|
|
return &r, nil
|
|
}
|
|
|