mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
182 lines
6.3 KiB
Go
182 lines
6.3 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package services
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
gitea "gitea.dev/sdk"
|
|
"gitea.dev/tea/internal/transport"
|
|
)
|
|
|
|
// Issue is the stable command representation of an issue.
|
|
type Issue struct {
|
|
Author string `json:"author"`
|
|
Labels []Label `json:"labels"`
|
|
Ref string `json:"ref"`
|
|
ID int64 `json:"id"`
|
|
Number int64 `json:"number"`
|
|
Kind string `json:"kind"`
|
|
Title string `json:"title"`
|
|
Body string `json:"body"`
|
|
URL string `json:"url"`
|
|
State string `json:"state"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
Deadline *string `json:"due_date"`
|
|
Assignees []string `json:"assignees"`
|
|
Milestone *IssueMilestone `json:"milestone"`
|
|
Comments int64 `json:"comments"`
|
|
}
|
|
|
|
// Kinds of Issue: Gitea lists pull requests among issues.
|
|
const (
|
|
KindIssue = "issue"
|
|
KindPull = "pr"
|
|
)
|
|
|
|
// discussionFrom converts an issue or a pull request listed among issues; the state of a merged
|
|
// pull request is "merged" rather than "closed".
|
|
func discussionFrom(issue *gitea.Issue) Issue {
|
|
item := issueFrom(issue)
|
|
item.Kind, item.State = KindIssue, discussionState(issue)
|
|
if issue.PullRequest != nil {
|
|
item.Kind = KindPull
|
|
}
|
|
return item
|
|
}
|
|
|
|
// discussionState is the state of an issue, or "merged" for a merged pull request.
|
|
func discussionState(issue *gitea.Issue) string {
|
|
if issue.PullRequest != nil && issue.PullRequest.HasMerged {
|
|
return "merged"
|
|
}
|
|
return string(issue.State)
|
|
}
|
|
|
|
// IssueMilestone identifies the milestone associated with an issue or pull request.
|
|
type IssueMilestone struct {
|
|
ID int64 `json:"id"`
|
|
Title string `json:"title"`
|
|
}
|
|
|
|
// IssueOptions supplies issue creation input.
|
|
type IssueOptions struct {
|
|
Ref string `json:"ref,omitempty"`
|
|
Title string `json:"title"`
|
|
Body string `json:"body"`
|
|
Assignees []string `json:"assignees,omitempty"`
|
|
Labels []int64 `json:"labels,omitempty"`
|
|
Milestone int64 `json:"milestone,omitempty"`
|
|
Deadline *time.Time `json:"due_date,omitempty"`
|
|
}
|
|
|
|
// ListOptions supplies explicit issue pagination and filters.
|
|
type ListOptions struct {
|
|
State string
|
|
Limit, Page int
|
|
Search string
|
|
Assignee, Labels, Milestones, Kind string
|
|
Author, Mentions string
|
|
Since, Before *time.Time
|
|
// NoAssignee keeps only issues without assignees. Gitea has no such filter, so ListPage
|
|
// applies it to every issue it reads.
|
|
NoAssignee bool
|
|
}
|
|
|
|
// Issues implements repository issue operations.
|
|
type Issues struct {
|
|
Client *transport.Client
|
|
Repository string
|
|
}
|
|
|
|
func (s Issues) endpoint() string { return "repos/" + s.Repository + "/issues" }
|
|
|
|
// List returns a page of issues, even when the server caps its page size below the limit,
|
|
// and reports whether more issues follow.
|
|
func (s Issues) List(ctx context.Context, opts ListOptions) ([]Issue, bool, error) {
|
|
page, err := s.ListPage(ctx, opts)
|
|
return page.Items, page.More, err
|
|
}
|
|
|
|
// ListPage is List with the number of issues that match, as the server counts them.
|
|
func (s Issues) ListPage(ctx context.Context, opts ListOptions) (Page[Issue], error) {
|
|
owner, repo, err := splitRepository(s.Repository)
|
|
if err != nil {
|
|
return Page[Issue]{}, err
|
|
}
|
|
kind := "issues"
|
|
if opts.Kind != "" {
|
|
kind = opts.Kind
|
|
}
|
|
query := gitea.ListIssueOption{
|
|
State: gitea.StateType(opts.State), Type: gitea.IssueType(kind), KeyWord: opts.Search,
|
|
Labels: filterValues(opts.Labels), Milestones: filterValues(opts.Milestones),
|
|
CreatedBy: opts.Author, AssignedBy: opts.Assignee, MentionedBy: opts.Mentions,
|
|
}
|
|
if opts.Since != nil {
|
|
query.Since = *opts.Since
|
|
}
|
|
if opts.Before != nil {
|
|
query.Before = *opts.Before
|
|
}
|
|
fetch := func(limit, page int) (Page[Issue], error) {
|
|
issues, err := callPage(ctx, s.Client, limit, page, func(ctx context.Context, c *gitea.Client, list gitea.ListOptions) ([]*gitea.Issue, *gitea.Response, error) {
|
|
query.ListOptions = list
|
|
return c.Issues.ListRepoIssues(ctx, owner, repo, query)
|
|
})
|
|
if err != nil {
|
|
return Page[Issue]{}, err
|
|
}
|
|
return convertPage(issues, discussionFrom), nil
|
|
}
|
|
if opts.NoAssignee {
|
|
return filteredPage(opts.Limit, opts.Page, fetch, func(item Issue) bool { return len(item.Assignees) == 0 })
|
|
}
|
|
return fetch(opts.Limit, opts.Page)
|
|
}
|
|
|
|
// filterValues splits a comma-separated filter, which the SDK joins again.
|
|
func filterValues(value string) []string {
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
return strings.Split(value, ",")
|
|
}
|
|
|
|
// View retrieves one issue.
|
|
func (s Issues) View(ctx context.Context, number int64) (Issue, error) {
|
|
item, err := repoConvert(ctx, s.Client, s.Repository, discussionFrom, func(ctx context.Context, c *gitea.Client, owner, repo string) (*gitea.Issue, *gitea.Response, error) {
|
|
return c.Issues.GetIssue(ctx, owner, repo, number)
|
|
})
|
|
return item, Classify(err, "issue", "#"+strconv.FormatInt(number, 10), s.Repository)
|
|
}
|
|
|
|
// Create opens an issue.
|
|
func (s Issues) Create(ctx context.Context, options IssueOptions) (Issue, error) {
|
|
item, err := repoConvert(ctx, s.Client, s.Repository, discussionFrom, func(ctx context.Context, c *gitea.Client, owner, repo string) (*gitea.Issue, *gitea.Response, error) {
|
|
return c.Issues.CreateIssue(ctx, owner, repo, gitea.CreateIssueOption{
|
|
Title: options.Title, Body: options.Body, Ref: options.Ref, Assignees: options.Assignees,
|
|
Deadline: options.Deadline, Milestone: options.Milestone, Labels: options.Labels,
|
|
})
|
|
})
|
|
return item, Classify(err, "issue", "", s.Repository)
|
|
}
|
|
|
|
// SetState updates only the selected issue state.
|
|
func (s Issues) SetState(ctx context.Context, number int64, state string) (Issue, error) {
|
|
value := gitea.StateType(state)
|
|
return s.edit(ctx, number, gitea.EditIssueOption{State: &value})
|
|
}
|
|
|
|
func (s Issues) edit(ctx context.Context, number int64, opts gitea.EditIssueOption) (Issue, error) {
|
|
item, err := repoConvert(ctx, s.Client, s.Repository, discussionFrom, func(ctx context.Context, c *gitea.Client, owner, repo string) (*gitea.Issue, *gitea.Response, error) {
|
|
return c.Issues.EditIssue(ctx, owner, repo, number, opts)
|
|
})
|
|
return item, Classify(err, "issue", "#"+strconv.FormatInt(number, 10), s.Repository)
|
|
}
|