mirror of
https://gitea.com/gitea/tea.git
synced 2026-09-10 07:26:33 -04:00
Follow-up to 5d3db5d (#571). No user-visible CLI changes.
- Replace tautological metadata unit tests with httptest-based e2e
tests that exercise runRequestReview, runCancelReview, and
runReviewersList against a mock Gitea API; each test captures
method, path, and request body.
- Add tests/integration/pulls_reviewers_test.go covering create-time
--reviewer/--team-reviewer, request-review add, cancel-review by
user and by team, validation error, and bad-PR error against a
real Gitea instance.
- Extract parseReviewRequestArgs in cmd/pulls/review_helpers.go so
runRequestReview and runCancelReview share one validation/parsing
path; both now route through task.ApplyReviewerChanges instead of
inlining the SDK call.
- Extend task.ApplyReviewerChanges to accept teamAdd/teamRm and use
it from request-review, cancel-review, and the existing edit flow.
- Replace task.CreatePull 8-positional-arg signature with a
CreatePullOptions struct; update cmd/pulls/create.go and
modules/interact/pull_create.go call sites.
- Defensively filter empty CSV entries in parseReviewRequestArgs via
nonEmptyValues so the validation is correct regardless of whether
the underlying CsvFlag trims empty entries (kept independent of
the separate CsvFlag bugfix PR).
Verification:
make fmt fmt-check vet lint test docs docs-check build
239 lines
6.4 KiB
Go
239 lines
6.4 KiB
Go
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package task
|
|
|
|
import (
|
|
stdctx "context"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
gitea "gitea.dev/sdk"
|
|
|
|
"gitea.dev/tea/modules/config"
|
|
"gitea.dev/tea/modules/context"
|
|
local_git "gitea.dev/tea/modules/git"
|
|
"gitea.dev/tea/modules/print"
|
|
"gitea.dev/tea/modules/utils"
|
|
)
|
|
|
|
var (
|
|
spaceRegex = regexp.MustCompile(`[\s_-]+`)
|
|
noSpace = regexp.MustCompile(`^[^a-zA-Z\s]*`)
|
|
consecutive = regexp.MustCompile(`[\s]{2,}`)
|
|
)
|
|
|
|
// CreatePullOptions bundles the inputs to CreatePull. The struct form keeps
|
|
// call sites readable as the API surface grows (and avoids the
|
|
// positional-arg drift that earlier PRs caused).
|
|
type CreatePullOptions struct {
|
|
Base string
|
|
Head string
|
|
AllowMaintainerEdits *bool
|
|
Issue *gitea.CreateIssueOption
|
|
Reviewers []string
|
|
TeamReviewers []string
|
|
}
|
|
|
|
// CreatePull creates a PR in the given repo and prints the result
|
|
func CreatePull(requestCtx stdctx.Context, ctx *context.TeaContext, in CreatePullOptions) (err error) {
|
|
base := in.Base
|
|
head := in.Head
|
|
opts := in.Issue
|
|
|
|
// default is default branch
|
|
if len(base) == 0 {
|
|
base, err = GetDefaultPRBase(requestCtx, ctx.Login, ctx.Owner, ctx.Repo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// default is current one
|
|
if len(head) == 0 {
|
|
if ctx.LocalRepo == nil {
|
|
return fmt.Errorf("no local git repo detected, please specify head branch")
|
|
}
|
|
headOwner, headBranch, err := GetDefaultPRHead(ctx.LocalRepo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
head = GetHeadSpec(headOwner, headBranch, ctx.Owner)
|
|
}
|
|
|
|
// head & base may not be the same
|
|
if head == base {
|
|
return fmt.Errorf("can't create PR from %s to %s", head, base)
|
|
}
|
|
|
|
// default is head branch name
|
|
if len(opts.Title) == 0 {
|
|
opts.Title = GetDefaultPRTitle(head)
|
|
}
|
|
// title is required
|
|
if len(opts.Title) == 0 {
|
|
return fmt.Errorf("title is required")
|
|
}
|
|
|
|
client := ctx.Login.Client()
|
|
|
|
pr, _, err := client.PullRequests.CreatePullRequest(requestCtx, ctx.Owner, ctx.Repo, gitea.CreatePullRequestOption{
|
|
Head: head,
|
|
Base: base,
|
|
Title: opts.Title,
|
|
Body: opts.Body,
|
|
Assignees: opts.Assignees,
|
|
Reviewers: in.Reviewers,
|
|
TeamReviewers: in.TeamReviewers,
|
|
Labels: opts.Labels,
|
|
Milestone: opts.Milestone,
|
|
Deadline: opts.Deadline,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("could not create PR from %s to %s:%s: %s", head, ctx.Owner, base, err)
|
|
}
|
|
|
|
if in.AllowMaintainerEdits != nil && pr.AllowMaintainerEdit != *in.AllowMaintainerEdits {
|
|
pr, _, err = client.PullRequests.EditPullRequest(requestCtx, ctx.Owner, ctx.Repo, pr.Index, gitea.EditPullRequestOption{
|
|
AllowMaintainerEdit: in.AllowMaintainerEdits,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("could not enable maintainer edit on pull: %v", err)
|
|
}
|
|
}
|
|
|
|
print.PullDetails(pr, nil, nil)
|
|
|
|
return err
|
|
}
|
|
|
|
// GetDefaultPRBase retrieves the default base branch for the given repo
|
|
func GetDefaultPRBase(requestCtx stdctx.Context, login *config.Login, owner, repo string) (string, error) {
|
|
meta, _, err := login.Client().Repositories.GetRepo(requestCtx, owner, repo)
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not fetch repo meta: %s", err)
|
|
}
|
|
return meta.DefaultBranch, nil
|
|
}
|
|
|
|
// GetDefaultPRHead uses the currently checked out branch, tries to find a remote
|
|
// that has a branch with the same name, and extracts the owner from its URL.
|
|
// If no remote matches, owner is empty, meaning same as head repo owner.
|
|
func GetDefaultPRHead(localRepo *local_git.TeaRepo) (owner, branch string, err error) {
|
|
var sha string
|
|
if branch, sha, err = localRepo.TeaGetCurrentBranchNameAndSHA(); err != nil {
|
|
return
|
|
}
|
|
|
|
remote, err := localRepo.TeaFindBranchRemote(branch, sha)
|
|
if err != nil {
|
|
err = fmt.Errorf("could not determine remote for current branch: %s", err)
|
|
return
|
|
}
|
|
|
|
if remote == nil {
|
|
// if no remote branch is found for the local branch,
|
|
// we leave owner empty, meaning "use same repo as head" to gitea.
|
|
return
|
|
}
|
|
|
|
url, err := local_git.ParseURL(remote.Config().URLs[0])
|
|
if err != nil {
|
|
return
|
|
}
|
|
owner, _ = utils.GetOwnerAndRepo(url.Path, "")
|
|
return
|
|
}
|
|
|
|
// GetHeadSpec creates a head string as expected by gitea API
|
|
func GetHeadSpec(owner, branch, baseOwner string) string {
|
|
if len(owner) != 0 && owner != baseOwner {
|
|
return fmt.Sprintf("%s:%s", owner, branch)
|
|
}
|
|
return branch
|
|
}
|
|
|
|
// GetDefaultPRTitle transforms a string like a branchname to a readable text
|
|
func GetDefaultPRTitle(header string) string {
|
|
// Extract the part after the last colon in the input string
|
|
colonIndex := strings.LastIndex(header, ":")
|
|
if colonIndex != -1 {
|
|
header = header[colonIndex+1:]
|
|
}
|
|
|
|
title := noSpace.ReplaceAllString(header, "")
|
|
title = spaceRegex.ReplaceAllString(title, " ")
|
|
title = strings.TrimSpace(title)
|
|
title = strings.Title(strings.ToLower(title))
|
|
title = consecutive.ReplaceAllString(title, " ")
|
|
|
|
return title
|
|
}
|
|
|
|
// CreateAgitFlowPull creates a agit flow PR in the given repo and prints the result
|
|
func CreateAgitFlowPull(requestCtx stdctx.Context, ctx *context.TeaContext, remote, head, base, topic string,
|
|
opts *gitea.CreateIssueOption,
|
|
callback func(string) (string, error),
|
|
) (err error) {
|
|
// default is default branch
|
|
if len(base) == 0 {
|
|
base, err = GetDefaultPRBase(requestCtx, ctx.Login, ctx.Owner, ctx.Repo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// default is current one
|
|
if len(head) == 0 {
|
|
if ctx.LocalRepo == nil {
|
|
return fmt.Errorf("no local git repo detected, please specify topic branch")
|
|
}
|
|
headOwner, headBranch, err := GetDefaultPRHead(ctx.LocalRepo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
head = GetHeadSpec(headOwner, headBranch, ctx.Owner)
|
|
}
|
|
|
|
if len(remote) == 0 {
|
|
return fmt.Errorf("remote is required for agit flow PR")
|
|
}
|
|
|
|
if len(topic) == 0 {
|
|
topic = head
|
|
}
|
|
|
|
if head == base || topic == base {
|
|
return fmt.Errorf("can't create PR from %s to %s", topic, base)
|
|
}
|
|
|
|
// default is head branch name
|
|
if len(opts.Title) == 0 {
|
|
opts.Title = GetDefaultPRTitle(head)
|
|
}
|
|
// title is required
|
|
if len(opts.Title) == 0 {
|
|
return fmt.Errorf("title is required")
|
|
}
|
|
|
|
localRepo, err := local_git.RepoForWorkdir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
url, err := localRepo.RemoteURL(remote)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
auth, err := local_git.GetAuthForURL(url, ctx.Login.GetAccessToken(), ctx.Login.SSHKey, callback)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return localRepo.PushToCreatAgitFlowPR(remote, head, base, topic, opts.Title, opts.Body, auth)
|
|
}
|