mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
services.IsNotFound replaces the hand-rolled 404 checks, convertPage the copied page conversions and lastPathNumber the comment's own number parser. The List wrappers around ListPage of releases, branches, wiki pages, secrets and variables go, pull request filters resolve milestones through Milestones.FindOne like issues, and localFlagsChanged, boolOrder, Cell.Render, stateBadge and dueText replace their copies. Signed-off-by: bircni <bircni@icloud.com>
177 lines
6.1 KiB
Go
177 lines
6.1 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package services
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
issues, err := listPage(opts.Limit, opts.Page, func(limit, page int) ([]*gitea.Issue, http.Header, error) {
|
|
query.ListOptions = listOptions(limit, page)
|
|
issues, resp, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) ([]*gitea.Issue, *gitea.Response, error) {
|
|
return c.Issues.ListRepoIssues(ctx, owner, repo, query)
|
|
})
|
|
return issues, headerOf(resp), err
|
|
})
|
|
if err != nil {
|
|
return Page[Issue]{}, err
|
|
}
|
|
return convertPage(issues, discussionFrom), nil
|
|
}
|
|
|
|
// 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)
|
|
}
|