mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 11:01:25 +01:00
perf: drop wasted requests and reuse markdown renderers
pr edit with reviewers and other fields reads the pull request once, org delete counts only repositories, org view counts concurrently, team list skips the repository count of all-repository teams, pr diff --web builds the URL locally, pr list re-reads pull requests concurrently for review fields, and glamour renderers are built once per width and color. Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
8 files changed
+101
-44
No files matched your search
@@ -292,11 +292,11 @@ $ tea org delete acme-old --yes`,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
size, err := service.Size(cmd.Context(), org.Name)
|
repos, err := service.RepositoryCount(cmd.Context(), org.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if size.Repositories != nil && *size.Repositories > 0 {
|
if repos != nil && *repos > 0 {
|
||||||
return &services.ConflictError{Reason: services.ReasonOwnsRepos, Kind: "organization", Key: org.Name, Hint: "transfer or delete them first (see tea repo list " + org.Name + ")"}
|
return &services.ConflictError{Reason: services.ReasonOwnsRepos, Kind: "organization", Key: org.Name, Hint: "transfer or delete them first (see tea repo list " + org.Name + ")"}
|
||||||
}
|
}
|
||||||
if err := a.confirmAction(cmd, terminal.Confirmation{
|
if err := a.confirmAction(cmd, terminal.Confirmation{
|
||||||
|
|||||||
@@ -176,11 +176,7 @@ tea pr diff 13 | git apply works.`, Annotations: pullArgumentsAnnotation(textOut
|
|||||||
}
|
}
|
||||||
service := services.Pulls{Client: t.client, Repository: t.Repo}
|
service := services.Pulls{Client: t.client, Repository: t.Repo}
|
||||||
if web {
|
if web {
|
||||||
item, err := service.View(cmd.Context(), n)
|
return a.openBrowser(cmd, fmt.Sprintf("%s/pulls/%d/files", repositoryURL(t), n))
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return a.openBrowser(cmd, item.URL+"/files")
|
|
||||||
}
|
}
|
||||||
if nameOnly {
|
if nameOnly {
|
||||||
files, err := service.Files(cmd.Context(), n)
|
files, err := service.Files(cmd.Context(), n)
|
||||||
|
|||||||
@@ -78,12 +78,10 @@ each pull request, which tea only makes when --json names them.`, Args: argsExac
|
|||||||
}
|
}
|
||||||
items := window.Items
|
items := window.Items
|
||||||
if a.jsonSelects("reviewers", "reviewer_teams", "merged_by") {
|
if a.jsonSelects("reviewers", "reviewer_teams", "merged_by") {
|
||||||
for i, item := range items {
|
if items, err = service.ViewAll(cmd.Context(), items); err != nil {
|
||||||
if items[i], err = service.View(cmd.Context(), item.Number); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
noun := map[string]string{"open": "open pull requests", "closed": "closed pull requests", "merged": "merged pull requests", "all": "pull requests"}[state]
|
noun := map[string]string{"open": "open pull requests", "closed": "closed pull requests", "merged": "merged pull requests", "all": "pull requests"}[state]
|
||||||
what := noun + " in " + t.Repo
|
what := noun + " in " + t.Repo
|
||||||
empty := "no " + what
|
empty := "no " + what
|
||||||
|
|||||||
@@ -94,7 +94,8 @@ type OrganizationSize struct {
|
|||||||
Members, Teams, Repositories *int
|
Members, Teams, Repositories *int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Size counts the members, teams and repositories of an organization.
|
// Size counts the members, teams and repositories of an organization; a count the account cannot
|
||||||
|
// read is nil.
|
||||||
func (s Organizations) Size(ctx context.Context, name string) (OrganizationSize, error) {
|
func (s Organizations) Size(ctx context.Context, name string) (OrganizationSize, error) {
|
||||||
first := listOptions(1, 1)
|
first := listOptions(1, 1)
|
||||||
counts := []func(context.Context, *gitea.Client) (*gitea.Response, error){
|
counts := []func(context.Context, *gitea.Client) (*gitea.Response, error){
|
||||||
@@ -106,24 +107,40 @@ func (s Organizations) Size(ctx context.Context, name string) (OrganizationSize,
|
|||||||
_, resp, err := c.Organizations.ListOrgTeams(ctx, name, gitea.ListTeamsOptions{ListOptions: first})
|
_, resp, err := c.Organizations.ListOrgTeams(ctx, name, gitea.ListTeamsOptions{ListOptions: first})
|
||||||
return resp, err
|
return resp, err
|
||||||
},
|
},
|
||||||
func(ctx context.Context, c *gitea.Client) (*gitea.Response, error) {
|
orgRepos(name),
|
||||||
_, resp, err := c.Repositories.ListOrgRepos(ctx, name, gitea.ListOrgReposOptions{ListOptions: first})
|
|
||||||
return resp, err
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
totals := make([]*int, len(counts))
|
totals := make([]*int, len(counts))
|
||||||
for i, count := range counts {
|
_ = concurrently(len(counts), func(i int) error {
|
||||||
total, err := listTotal(ctx, s.Client, count)
|
totals[i], _ = listTotal(ctx, s.Client, counts[i])
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return OrganizationSize{}, ctx.Err()
|
return OrganizationSize{}, ctx.Err()
|
||||||
}
|
}
|
||||||
if err == nil {
|
|
||||||
totals[i] = total
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return OrganizationSize{Members: totals[0], Teams: totals[1], Repositories: totals[2]}, nil
|
return OrganizationSize{Members: totals[0], Teams: totals[1], Repositories: totals[2]}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RepositoryCount counts the repositories of an organization, or returns nil when the account
|
||||||
|
// cannot read the count.
|
||||||
|
func (s Organizations) RepositoryCount(ctx context.Context, name string) (*int, error) {
|
||||||
|
total, err := listTotal(ctx, s.Client, orgRepos(name))
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// orgRepos requests the first repository of an organization, to count them.
|
||||||
|
func orgRepos(name string) func(context.Context, *gitea.Client) (*gitea.Response, error) {
|
||||||
|
return func(ctx context.Context, c *gitea.Client) (*gitea.Response, error) {
|
||||||
|
_, resp, err := c.Repositories.ListOrgRepos(ctx, name, gitea.ListOrgReposOptions{ListOptions: listOptions(1, 1)})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// listTotal returns the collection size a list request reports, or nil when the server does not count it.
|
// listTotal returns the collection size a list request reports, or nil when the server does not count it.
|
||||||
func listTotal(ctx context.Context, client *transport.Client, list func(context.Context, *gitea.Client) (*gitea.Response, error)) (*int, error) {
|
func listTotal(ctx context.Context, client *transport.Client, list func(context.Context, *gitea.Client) (*gitea.Response, error)) (*int, error) {
|
||||||
_, resp, err := call(ctx, client, func(ctx context.Context, c *gitea.Client) (struct{}, *gitea.Response, error) {
|
_, resp, err := call(ctx, client, func(ctx context.Context, c *gitea.Client) (struct{}, *gitea.Response, error) {
|
||||||
|
|||||||
@@ -236,6 +236,17 @@ func (s Pulls) View(ctx context.Context, n int64) (Pull, error) {
|
|||||||
return pullFrom(pull), nil
|
return pullFrom(pull), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ViewAll retrieves each of items again, a few at a time, for the fields only a single pull
|
||||||
|
// request carries, such as its reviewers.
|
||||||
|
func (s Pulls) ViewAll(ctx context.Context, items []Pull) ([]Pull, error) {
|
||||||
|
viewed := make([]Pull, len(items))
|
||||||
|
err := concurrently(len(items), func(i int) (err error) {
|
||||||
|
viewed[i], err = s.View(ctx, items[i].Number)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return viewed, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s Pulls) get(ctx context.Context, n int64) (*gitea.PullRequest, error) {
|
func (s Pulls) get(ctx context.Context, n int64) (*gitea.PullRequest, error) {
|
||||||
item, err := repoCall(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, name string) (*gitea.PullRequest, *gitea.Response, error) {
|
item, err := repoCall(ctx, s.Client, s.Repository, func(ctx context.Context, c *gitea.Client, owner, name string) (*gitea.PullRequest, *gitea.Response, error) {
|
||||||
return c.PullRequests.GetPullRequest(ctx, owner, name, n)
|
return c.PullRequests.GetPullRequest(ctx, owner, name, n)
|
||||||
@@ -264,15 +275,15 @@ func (s Pulls) Create(ctx context.Context, opts PullOptions) (Pull, error) {
|
|||||||
// Edit updates only explicitly supplied resource fields.
|
// Edit updates only explicitly supplied resource fields.
|
||||||
func (s Pulls) Edit(ctx context.Context, n int64, opts PullEdit) (Pull, error) {
|
func (s Pulls) Edit(ctx context.Context, n int64, opts PullEdit) (Pull, error) {
|
||||||
if opts.ReviewerChanges != nil {
|
if opts.ReviewerChanges != nil {
|
||||||
item, err := (Reviews{Client: s.Client, Repository: s.Repository}).ChangeRequests(ctx, n, *opts.ReviewerChanges)
|
reviews, changes := Reviews{Client: s.Client, Repository: s.Repository}, opts.ReviewerChanges
|
||||||
if err != nil {
|
|
||||||
return Pull{}, err
|
|
||||||
}
|
|
||||||
opts.ReviewerChanges = nil
|
opts.ReviewerChanges = nil
|
||||||
if opts == (PullEdit{}) {
|
if opts == (PullEdit{}) {
|
||||||
return item, nil
|
return reviews.ChangeRequests(ctx, n, *changes)
|
||||||
}
|
}
|
||||||
item, err = s.Edit(ctx, n, opts)
|
if err := reviews.changeRequests(ctx, n, *changes); err != nil {
|
||||||
|
return Pull{}, err
|
||||||
|
}
|
||||||
|
item, err := s.Edit(ctx, n, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Pull{}, fmt.Errorf("review requests were updated, but editing other pull request fields failed: %w", err)
|
return Pull{}, fmt.Errorf("review requests were updated, but editing other pull request fields failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,12 +51,24 @@ func (c ReviewRequestChanges) Validate() error {
|
|||||||
|
|
||||||
// ChangeRequests returns the PR after changes and reports partial mutation failures.
|
// ChangeRequests returns the PR after changes and reports partial mutation failures.
|
||||||
func (s Reviews) ChangeRequests(ctx context.Context, n int64, changes ReviewRequestChanges) (Pull, error) {
|
func (s Reviews) ChangeRequests(ctx context.Context, n int64, changes ReviewRequestChanges) (Pull, error) {
|
||||||
if err := changes.Validate(); err != nil {
|
if err := s.changeRequests(ctx, n, changes); err != nil {
|
||||||
return Pull{}, err
|
return Pull{}, err
|
||||||
}
|
}
|
||||||
|
item, err := (Pulls{Client: s.Client, Repository: s.Repository}).View(ctx, n)
|
||||||
|
if err != nil {
|
||||||
|
return Pull{}, fmt.Errorf("review requests were updated, but reading the pull request failed: %w", err)
|
||||||
|
}
|
||||||
|
return item, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// changeRequests applies the review request changes to pull request n.
|
||||||
|
func (s Reviews) changeRequests(ctx context.Context, n int64, changes ReviewRequestChanges) error {
|
||||||
|
if err := changes.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if len(changes.Add.Teams)+len(changes.Remove.Teams) > 0 {
|
if len(changes.Add.Teams)+len(changes.Remove.Teams) > 0 {
|
||||||
if err := s.CheckRequestTeams(ctx, append(append([]string{}, changes.Add.Teams...), changes.Remove.Teams...)); err != nil {
|
if err := s.CheckRequestTeams(ctx, append(append([]string{}, changes.Add.Teams...), changes.Remove.Teams...)); err != nil {
|
||||||
return Pull{}, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mutated := false
|
mutated := false
|
||||||
@@ -76,17 +88,13 @@ func (s Reviews) ChangeRequests(ctx context.Context, n int64, changes ReviewRequ
|
|||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if mutated {
|
if mutated {
|
||||||
return Pull{}, fmt.Errorf("reviewer removals succeeded, but adding reviewers failed: %w", err)
|
return fmt.Errorf("reviewer removals succeeded, but adding reviewers failed: %w", err)
|
||||||
}
|
}
|
||||||
return Pull{}, fmt.Errorf("review request update failed; Gitea may have applied some requests: %w", err)
|
return fmt.Errorf("review request update failed; Gitea may have applied some requests: %w", err)
|
||||||
}
|
}
|
||||||
mutated = true
|
mutated = true
|
||||||
}
|
}
|
||||||
item, err := (Pulls{Client: s.Client, Repository: s.Repository}).View(ctx, n)
|
return nil
|
||||||
if err != nil {
|
|
||||||
return Pull{}, fmt.Errorf("review requests were updated, but reading the pull request failed: %w", err)
|
|
||||||
}
|
|
||||||
return item, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckRequestTeams verifies that every named team exists in the repository's organization.
|
// CheckRequestTeams verifies that every named team exists in the repository's organization.
|
||||||
|
|||||||
@@ -203,7 +203,8 @@ func (s Teams) Sizes(ctx context.Context, teams []Team) ([]TeamSize, error) {
|
|||||||
_, resp, err := c.Organizations.ListTeamMembers(ctx, id, gitea.ListTeamMembersOptions{ListOptions: first})
|
_, resp, err := c.Organizations.ListTeamMembers(ctx, id, gitea.ListTeamMembersOptions{ListOptions: first})
|
||||||
return resp, err
|
return resp, err
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil || teams[i].AllRepositories {
|
||||||
|
sizes[i] = TeamSize{Members: members}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
repositories, err := listTotal(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.Response, error) {
|
repositories, err := listTotal(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.Response, error) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package terminal
|
|||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"charm.land/glamour/v2"
|
"charm.land/glamour/v2"
|
||||||
"charm.land/glamour/v2/styles"
|
"charm.land/glamour/v2/styles"
|
||||||
@@ -19,6 +20,34 @@ func MarkdownEnabled(preference string, outTTY bool) bool {
|
|||||||
// glamourLinks matches the OSC 8 hyperlinks that glamour writes for markdown links.
|
// glamourLinks matches the OSC 8 hyperlinks that glamour writes for markdown links.
|
||||||
var glamourLinks = regexp.MustCompile("\x1b\\]8;[^\x07\x1b]*(?:\x07|\x1b\\\\)")
|
var glamourLinks = regexp.MustCompile("\x1b\\]8;[^\x07\x1b]*(?:\x07|\x1b\\\\)")
|
||||||
|
|
||||||
|
// renderers caches a glamour renderer per width and color, as building one parses its style each
|
||||||
|
// time; the lock also serializes rendering.
|
||||||
|
var renderers = struct {
|
||||||
|
sync.Mutex
|
||||||
|
byKey map[[2]int]*glamour.TermRenderer
|
||||||
|
}{byKey: map[[2]int]*glamour.TermRenderer{}}
|
||||||
|
|
||||||
|
func markdownRenderer(width int, color bool) (*glamour.TermRenderer, error) {
|
||||||
|
key := [2]int{width, 0}
|
||||||
|
if color {
|
||||||
|
key[1] = 1
|
||||||
|
}
|
||||||
|
if renderer := renderers.byKey[key]; renderer != nil {
|
||||||
|
return renderer, nil
|
||||||
|
}
|
||||||
|
style := styles.NoTTYStyleConfig
|
||||||
|
if color {
|
||||||
|
style = styles.DarkStyleConfig
|
||||||
|
// The terminal's own text color keeps body text readable on light backgrounds too.
|
||||||
|
style.Document.Color = nil
|
||||||
|
}
|
||||||
|
renderer, err := glamour.NewTermRenderer(glamour.WithStyles(style), glamour.WithWordWrap(width), glamour.WithPreservedNewLines())
|
||||||
|
if err == nil {
|
||||||
|
renderers.byKey[key] = renderer
|
||||||
|
}
|
||||||
|
return renderer, err
|
||||||
|
}
|
||||||
|
|
||||||
// Markdown renders sanitized markdown for d: wrapped to its width (80 when unknown), colored or plain,
|
// Markdown renders sanitized markdown for d: wrapped to its width (80 when unknown), colored or plain,
|
||||||
// with links only when d allows them. It keeps line breaks like Gitea does, and returns the sanitized
|
// with links only when d allows them. It keeps line breaks like Gitea does, and returns the sanitized
|
||||||
// text unrendered if rendering fails.
|
// text unrendered if rendering fails.
|
||||||
@@ -28,17 +57,14 @@ func Markdown(text string, d Display) string {
|
|||||||
if width <= 0 {
|
if width <= 0 {
|
||||||
width = 80
|
width = 80
|
||||||
}
|
}
|
||||||
style := styles.NoTTYStyleConfig
|
renderers.Lock()
|
||||||
if d.Color {
|
renderer, err := markdownRenderer(width, d.Color)
|
||||||
style = styles.DarkStyleConfig
|
|
||||||
// The terminal's own text color keeps body text readable on light backgrounds too.
|
|
||||||
style.Document.Color = nil
|
|
||||||
}
|
|
||||||
renderer, err := glamour.NewTermRenderer(glamour.WithStyles(style), glamour.WithWordWrap(width), glamour.WithPreservedNewLines())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
renderers.Unlock()
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
rendered, err := renderer.Render(text)
|
rendered, err := renderer.Render(text)
|
||||||
|
renderers.Unlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user