feat: vendor gitea 1.16.2
This commit is contained in:
@@ -7,6 +7,7 @@ package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"slices"
|
||||
@@ -20,6 +21,8 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/references"
|
||||
@@ -233,11 +236,17 @@ func (r RoleInRepo) LocaleHelper(lang translation.Locale) string {
|
||||
return lang.TrString("repo.issues.role." + string(r) + "_helper")
|
||||
}
|
||||
|
||||
type SpecialDoerNameType string
|
||||
|
||||
const SpecialDoerNameCodeOwners SpecialDoerNameType = "CODEOWNERS"
|
||||
|
||||
// CommentMetaData stores metadata for a comment, these data will not be changed once inserted into database
|
||||
type CommentMetaData struct {
|
||||
ProjectColumnID int64 `json:"project_column_id,omitempty"`
|
||||
ProjectColumnTitle string `json:"project_column_title,omitempty"`
|
||||
ProjectTitle string `json:"project_title,omitempty"`
|
||||
|
||||
SpecialDoerName SpecialDoerNameType `json:"special_doer_name,omitempty"` // e.g. "CODEOWNERS" for CODEOWNERS-triggered review requests
|
||||
}
|
||||
|
||||
// Comment represents a comment in commit and issue page.
|
||||
@@ -319,21 +328,34 @@ type Comment struct {
|
||||
RefIssue *Issue `xorm:"-"`
|
||||
RefComment *Comment `xorm:"-"`
|
||||
|
||||
Commits []*git_model.SignCommitWithStatuses `xorm:"-"`
|
||||
OldCommit string `xorm:"-"`
|
||||
NewCommit string `xorm:"-"`
|
||||
CommitsNum int64 `xorm:"-"`
|
||||
IsForcePush bool `xorm:"-"`
|
||||
Commits []*git_model.SignCommitWithStatuses `xorm:"-"`
|
||||
OldCommit string `xorm:"-"`
|
||||
NewCommit string `xorm:"-"`
|
||||
CommitsNum int64 `xorm:"-"`
|
||||
|
||||
// Templates still use it. It is not persisted in database, it is only set when creating or loading
|
||||
IsForcePush bool `xorm:"-"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(Comment))
|
||||
}
|
||||
|
||||
// PushActionContent is content of push pull comment
|
||||
// PushActionContent is content of pull request's push comment
|
||||
type PushActionContent struct {
|
||||
IsForcePush bool `json:"is_force_push"`
|
||||
CommitIDs []string `json:"commit_ids"`
|
||||
IsForcePush bool `json:"is_force_push"`
|
||||
// if IsForcePush=true, CommitIDs contains the commit pair [old head, new head]
|
||||
// if IsForcePush=false, CommitIDs contains the new commits newly pushed to the head branch
|
||||
CommitIDs []string `json:"commit_ids"`
|
||||
}
|
||||
|
||||
func (c *Comment) GetPushActionContent() (*PushActionContent, error) {
|
||||
if c.Type != CommentTypePullRequestPush {
|
||||
return nil, errors.New("not a pull request push comment")
|
||||
}
|
||||
var data PushActionContent
|
||||
_ = json.Unmarshal(util.UnsafeStringToBytes(c.Content), &data)
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
// LoadIssue loads the issue reference for the comment
|
||||
@@ -377,16 +399,7 @@ func (c *Comment) LoadPoster(ctx context.Context) (err error) {
|
||||
if c.Poster != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.Poster, err = user_model.GetPossibleUserByID(ctx, c.PosterID)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
c.PosterID = user_model.GhostUserID
|
||||
c.Poster = user_model.NewGhostUser()
|
||||
} else {
|
||||
log.Error("getUserByID[%d]: %v", c.ID, err)
|
||||
}
|
||||
}
|
||||
c.PosterID, c.Poster, err = user_model.GetPossibleUserByID(ctx, c.PosterID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -692,7 +705,7 @@ func (c *Comment) LoadTime(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
c.Time, err = GetTrackedTimeByID(ctx, c.TimeID)
|
||||
c.Time, err = GetTrackedTimeByID(ctx, c.IssueID, c.TimeID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -764,6 +777,37 @@ func (c *Comment) CodeCommentLink(ctx context.Context) string {
|
||||
return fmt.Sprintf("%s/files#%s", c.Issue.Link(), c.HashTag())
|
||||
}
|
||||
|
||||
func (c *Comment) MetaSpecialDoerTr(locale translation.Locale) template.HTML {
|
||||
if c.CommentMetaData == nil {
|
||||
return ""
|
||||
}
|
||||
if c.CommentMetaData.SpecialDoerName == SpecialDoerNameCodeOwners {
|
||||
return locale.Tr("repo.issues.review.codeowners_rules")
|
||||
}
|
||||
return htmlutil.HTMLFormat("%s", c.CommentMetaData.SpecialDoerName)
|
||||
}
|
||||
|
||||
func (c *Comment) TimelineRequestedReviewTr(locale translation.Locale, createdStr template.HTML) template.HTML {
|
||||
if c.AssigneeID > 0 {
|
||||
// it guarantees LoadAssigneeUserAndTeam has been called, and c.Assignee is Ghost user but not nil if the user doesn't exist
|
||||
if c.RemovedAssignee {
|
||||
if c.PosterID == c.AssigneeID {
|
||||
return locale.Tr("repo.issues.review.remove_review_request_self", createdStr)
|
||||
}
|
||||
return locale.Tr("repo.issues.review.remove_review_request", c.Assignee.GetDisplayName(), createdStr)
|
||||
}
|
||||
return locale.Tr("repo.issues.review.add_review_request", c.Assignee.GetDisplayName(), createdStr)
|
||||
}
|
||||
teamName := "Ghost Team"
|
||||
if c.AssigneeTeam != nil {
|
||||
teamName = c.AssigneeTeam.Name
|
||||
}
|
||||
if c.RemovedAssignee {
|
||||
return locale.Tr("repo.issues.review.remove_review_request", teamName, createdStr)
|
||||
}
|
||||
return locale.Tr("repo.issues.review.add_review_request", teamName, createdStr)
|
||||
}
|
||||
|
||||
// CreateComment creates comment with context
|
||||
func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, err error) {
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) {
|
||||
@@ -780,6 +824,11 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment,
|
||||
ProjectTitle: opts.ProjectTitle,
|
||||
}
|
||||
}
|
||||
if opts.SpecialDoerName != "" {
|
||||
commentMetaData = &CommentMetaData{
|
||||
SpecialDoerName: opts.SpecialDoerName,
|
||||
}
|
||||
}
|
||||
|
||||
comment := &Comment{
|
||||
Type: opts.Type,
|
||||
@@ -976,6 +1025,7 @@ type CreateCommentOptions struct {
|
||||
RefIsPull bool
|
||||
IsForcePush bool
|
||||
Invalidated bool
|
||||
SpecialDoerName SpecialDoerNameType // e.g. "CODEOWNERS" for CODEOWNERS-triggered review requests
|
||||
}
|
||||
|
||||
// GetCommentByID returns the comment by given ID.
|
||||
@@ -990,6 +1040,20 @@ func GetCommentByID(ctx context.Context, id int64) (*Comment, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func GetCommentWithRepoID(ctx context.Context, repoID, commentID int64) (*Comment, error) {
|
||||
c, err := GetCommentByID(ctx, commentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.LoadIssue(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.Issue.RepoID != repoID {
|
||||
return nil, ErrCommentNotExist{commentID, 0}
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// FindCommentsOptions describes the conditions to Find comments
|
||||
type FindCommentsOptions struct {
|
||||
db.ListOptions
|
||||
|
||||
@@ -102,6 +102,7 @@ func findCodeComments(ctx context.Context, opts FindCommentsOptions, issue *Issu
|
||||
continue
|
||||
}
|
||||
comment.Review = re
|
||||
comment.Issue = issue
|
||||
}
|
||||
comments[n] = comment
|
||||
n++
|
||||
|
||||
@@ -190,17 +190,10 @@ func (issue *Issue) IsTimetrackerEnabled(ctx context.Context) bool {
|
||||
|
||||
// LoadPoster loads poster
|
||||
func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
|
||||
if issue.Poster == nil && issue.PosterID != 0 {
|
||||
issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID)
|
||||
if err != nil {
|
||||
issue.PosterID = user_model.GhostUserID
|
||||
issue.Poster = user_model.NewGhostUser()
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
return fmt.Errorf("getUserByID.(poster) [%d]: %w", issue.PosterID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if issue.Poster != nil {
|
||||
return nil
|
||||
}
|
||||
issue.PosterID, issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -498,7 +491,7 @@ func (issue *Issue) GetLastComment(ctx context.Context) (*Comment, error) {
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
@@ -682,7 +675,7 @@ func (issue *Issue) GetParticipantIDsByIssue(ctx context.Context) ([]int64, erro
|
||||
}
|
||||
|
||||
// BlockedByDependencies finds all Dependencies an issue is blocked by
|
||||
func (issue *Issue) BlockedByDependencies(ctx context.Context, opts db.ListOptions) (issueDeps []*DependencyInfo, err error) {
|
||||
func (issue *Issue) BlockedByDependencies(ctx context.Context, opts db.ListOptions) (issueDeps []*DependencyInfo, total int64, err error) {
|
||||
sess := db.GetEngine(ctx).
|
||||
Table("issue").
|
||||
Join("INNER", "repository", "repository.id = issue.repo_id").
|
||||
@@ -693,13 +686,13 @@ func (issue *Issue) BlockedByDependencies(ctx context.Context, opts db.ListOptio
|
||||
if opts.Page > 0 {
|
||||
sess = db.SetSessionPagination(sess, &opts)
|
||||
}
|
||||
err = sess.Find(&issueDeps)
|
||||
total, err = sess.FindAndCount(&issueDeps)
|
||||
|
||||
for _, depInfo := range issueDeps {
|
||||
depInfo.Issue.Repo = &depInfo.Repository
|
||||
}
|
||||
|
||||
return issueDeps, err
|
||||
return issueDeps, total, err
|
||||
}
|
||||
|
||||
// BlockingDependencies returns all blocking dependencies, aka all other issues a given issue blocks
|
||||
|
||||
@@ -356,7 +356,7 @@ func ClearIssueLabels(ctx context.Context, issue *Issue, doer *user_model.User)
|
||||
return err
|
||||
}
|
||||
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
|
||||
perm, err := access_model.GetDoerRepoPermission(ctx, issue.Repo, doer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -64,36 +64,6 @@ func LoadProjectIssueColumnMap(ctx context.Context, projectID, defaultColumnID i
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// LoadIssuesFromColumn load issues assigned to this column
|
||||
func LoadIssuesFromColumn(ctx context.Context, b *project_model.Column, opts *IssuesOptions) (IssueList, error) {
|
||||
issueList, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) {
|
||||
o.ProjectColumnID = b.ID
|
||||
o.ProjectID = b.ProjectID
|
||||
o.SortType = "project-column-sorting"
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if b.Default {
|
||||
issues, err := Issues(ctx, opts.Copy(func(o *IssuesOptions) {
|
||||
o.ProjectColumnID = db.NoConditionID
|
||||
o.ProjectID = b.ProjectID
|
||||
o.SortType = "project-column-sorting"
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issueList = append(issueList, issues...)
|
||||
}
|
||||
|
||||
if err := issueList.LoadComments(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return issueList, nil
|
||||
}
|
||||
|
||||
// IssueAssignOrRemoveProject changes the project associated with an issue
|
||||
// If newProjectID is 0, the issue is removed from the project
|
||||
func IssueAssignOrRemoveProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID, newColumnID int64) error {
|
||||
|
||||
@@ -665,9 +665,9 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u
|
||||
continue
|
||||
}
|
||||
// Normal users must have read access to the referencing issue
|
||||
perm, err := access_model.GetUserRepoPermission(ctx, issue.Repo, user)
|
||||
perm, err := access_model.GetIndividualUserRepoPermission(ctx, issue.Repo, user)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetUserRepoPermission [%d]: %w", user.ID, err)
|
||||
return nil, fmt.Errorf("GetIndividualUserRepoPermission [%d]: %w", user.ID, err)
|
||||
}
|
||||
if !perm.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
continue
|
||||
|
||||
@@ -207,7 +207,7 @@ func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossRefe
|
||||
|
||||
// Check doer permissions; set action to None if the doer can't change the destination
|
||||
if refIssue.RepoID != ctx.OrigIssue.RepoID || ref.Action != references.XRefActionNone {
|
||||
perm, err := access_model.GetUserRepoPermission(stdCtx, refIssue.Repo, ctx.Doer)
|
||||
perm, err := access_model.GetDoerRepoPermission(stdCtx, refIssue.Repo, ctx.Doer)
|
||||
if err != nil {
|
||||
return nil, references.XRefActionNone, err
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ func TestGetLabelsByIssueID(t *testing.T) {
|
||||
func TestUpdateLabel(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
|
||||
// make sure update wont overwrite it
|
||||
// make sure update won't overwrite it
|
||||
update := &issues_model.Label{
|
||||
ID: label.ID,
|
||||
Color: "#ffff00",
|
||||
|
||||
@@ -75,6 +75,8 @@ func init() {
|
||||
func (m *Milestone) BeforeUpdate() {
|
||||
if m.NumIssues > 0 {
|
||||
m.Completeness = m.NumClosedIssues * 100 / m.NumIssues
|
||||
} else if m.IsClosed {
|
||||
m.Completeness = 100
|
||||
} else {
|
||||
m.Completeness = 0
|
||||
}
|
||||
@@ -195,8 +197,8 @@ func UpdateMilestoneCounters(ctx context.Context, id int64) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = e.Exec("UPDATE `milestone` SET completeness=100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) WHERE id=?",
|
||||
id,
|
||||
_, err = e.Exec("UPDATE `milestone` SET completeness=(CASE WHEN is_closed = ? AND num_issues = 0 THEN 100 ELSE 100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) END) WHERE id=?",
|
||||
true, id,
|
||||
)
|
||||
return err
|
||||
}
|
||||
@@ -240,6 +242,11 @@ func changeMilestoneStatus(ctx context.Context, m *Milestone, isClosed bool) err
|
||||
if count < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := UpdateMilestoneCounters(ctx, m.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return updateRepoMilestoneNum(ctx, m.RepoID)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,18 @@ func (milestones MilestoneList) getMilestoneIDs() []int64 {
|
||||
return ids
|
||||
}
|
||||
|
||||
// SplitByOpenClosed splits the milestone list into open and closed milestones
|
||||
func (milestones MilestoneList) SplitByOpenClosed() (open, closed MilestoneList) {
|
||||
for _, m := range milestones {
|
||||
if m.IsClosed {
|
||||
closed = append(closed, m)
|
||||
} else {
|
||||
open = append(open, m)
|
||||
}
|
||||
}
|
||||
return open, closed
|
||||
}
|
||||
|
||||
// FindMilestoneOptions contain options to get milestones
|
||||
type FindMilestoneOptions struct {
|
||||
db.ListOptions
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
@@ -24,6 +23,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/dlclark/regexp2"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
@@ -417,10 +417,6 @@ func (pr *PullRequest) GetGitHeadRefName() string {
|
||||
return fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index)
|
||||
}
|
||||
|
||||
func (pr *PullRequest) GetGitHeadBranchRefName() string {
|
||||
return fmt.Sprintf("%s%s", git.BranchPrefix, pr.HeadBranch)
|
||||
}
|
||||
|
||||
// GetReviewCommentsCount returns the number of review comments made on the diff of a PR review (not including comments on commits or issues in a PR)
|
||||
func (pr *PullRequest) GetReviewCommentsCount(ctx context.Context) int {
|
||||
opts := FindCommentsOptions{
|
||||
@@ -646,9 +642,8 @@ func (pr *PullRequest) UpdateCols(ctx context.Context, cols ...string) error {
|
||||
}
|
||||
|
||||
// UpdateColsIfNotMerged updates specific fields of a pull request if it has not been merged
|
||||
func (pr *PullRequest) UpdateColsIfNotMerged(ctx context.Context, cols ...string) error {
|
||||
_, err := db.GetEngine(ctx).Where("id = ? AND has_merged = ?", pr.ID, false).Cols(cols...).Update(pr)
|
||||
return err
|
||||
func (pr *PullRequest) UpdateColsIfNotMerged(ctx context.Context, cols ...string) (int64, error) {
|
||||
return db.GetEngine(ctx).Where("id = ? AND has_merged = ?", pr.ID, false).Cols(cols...).Update(pr)
|
||||
}
|
||||
|
||||
// IsWorkInProgress determine if the Pull Request is a Work In Progress by its title
|
||||
@@ -663,17 +658,24 @@ func (pr *PullRequest) IsWorkInProgress(ctx context.Context) bool {
|
||||
|
||||
// HasWorkInProgressPrefix determines if the given PR title has a Work In Progress prefix
|
||||
func HasWorkInProgressPrefix(title string) bool {
|
||||
for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes {
|
||||
if strings.HasPrefix(strings.ToUpper(title), strings.ToUpper(prefix)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
_, ok := CutWorkInProgressPrefix(title)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsFilesConflicted determines if the Pull Request has changes conflicting with the target branch.
|
||||
func CutWorkInProgressPrefix(title string) (origTitle string, ok bool) {
|
||||
for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes {
|
||||
prefixLen := len(prefix)
|
||||
if prefixLen <= len(title) && util.AsciiEqualFold(title[:prefixLen], prefix) {
|
||||
return title[len(prefix):], true
|
||||
}
|
||||
}
|
||||
return title, false
|
||||
}
|
||||
|
||||
// IsFilesConflicted determines if the Pull Request has changes conflicting with the target branch.
|
||||
// Sometimes a conflict may not list any files
|
||||
func (pr *PullRequest) IsFilesConflicted() bool {
|
||||
return len(pr.ConflictedFiles) > 0
|
||||
return pr.Status == PullRequestStatusConflict
|
||||
}
|
||||
|
||||
// GetWorkInProgressPrefix returns the prefix used to mark the pull request as a work in progress.
|
||||
@@ -859,7 +861,7 @@ func GetCodeOwnersFromContent(ctx context.Context, data string) ([]*CodeOwnerRul
|
||||
}
|
||||
|
||||
type CodeOwnerRule struct {
|
||||
Rule *regexp.Regexp
|
||||
Rule *regexp2.Regexp // it supports negative lookahead, does better for end users
|
||||
Negative bool
|
||||
Users []*user_model.User
|
||||
Teams []*org_model.Team
|
||||
@@ -875,7 +877,13 @@ func ParseCodeOwnersLine(ctx context.Context, tokens []string) (*CodeOwnerRule,
|
||||
|
||||
warnings := make([]string, 0)
|
||||
|
||||
rule.Rule, err = regexp.Compile(fmt.Sprintf("^%s$", strings.TrimPrefix(tokens[0], "!")))
|
||||
// Strip leading "!" for negative rules, then strip leading "/" since
|
||||
// git returns relative paths (e.g. "docs/foo.md" not "/docs/foo.md")
|
||||
// and the regex is already anchored with ^...$, so the "/" is redundant.
|
||||
pattern := strings.TrimPrefix(tokens[0], "!")
|
||||
pattern = strings.TrimPrefix(pattern, "/")
|
||||
expr := fmt.Sprintf("^%s$", pattern)
|
||||
rule.Rule, err = regexp2.Compile(expr, regexp2.None)
|
||||
if err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("incorrect codeowner regexp: %s", err))
|
||||
return nil, warnings
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
@@ -70,38 +71,69 @@ func GetUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch
|
||||
}
|
||||
|
||||
// CanMaintainerWriteToBranch check whether user is a maintainer and could write to the branch
|
||||
func CanMaintainerWriteToBranch(ctx context.Context, p access_model.Permission, branch string, user *user_model.User) bool {
|
||||
if p.CanWrite(unit.TypeCode) {
|
||||
return true
|
||||
func CanMaintainerWriteToBranch(ctx context.Context, headPerm access_model.Permission, headBranch string, doer *user_model.User) bool {
|
||||
can, err := canMaintainerWriteToBranch(ctx, headPerm, headBranch, doer)
|
||||
if err != nil {
|
||||
log.Error("CanMaintainerWriteToBranch: %v", err)
|
||||
return false
|
||||
}
|
||||
return can
|
||||
}
|
||||
|
||||
func canMaintainerWriteToBranch(ctx context.Context, headPerm access_model.Permission, headBranch string, doer *user_model.User) (bool, error) {
|
||||
if headPerm.CanWrite(unit.TypeCode) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// the code below depends on units to get the repository ID, not ideal but just keep it for now
|
||||
firstUnitRepoID := p.GetFirstUnitRepoID()
|
||||
firstUnitRepoID := headPerm.GetFirstUnitRepoID()
|
||||
if firstUnitRepoID == 0 {
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, firstUnitRepoID, branch)
|
||||
prs, err := GetUnmergedPullRequestsByHeadInfo(ctx, firstUnitRepoID, headBranch)
|
||||
if err != nil {
|
||||
return false
|
||||
return false, err
|
||||
}
|
||||
if _, err := prs.LoadIssues(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, pr := range prs {
|
||||
if pr.AllowMaintainerEdit {
|
||||
err = pr.LoadBaseRepo(ctx)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
prPerm, err := access_model.GetUserRepoPermission(ctx, pr.BaseRepo, user)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if prPerm.CanWrite(unit.TypeCode) {
|
||||
return true
|
||||
}
|
||||
if !pr.AllowMaintainerEdit {
|
||||
continue
|
||||
}
|
||||
|
||||
// check the PR's poster's permissions
|
||||
// If a "reader" poster created the PR in base repo from head repo, even if it is allowed to be edited by maintainers,
|
||||
// the maintainers should not be allowed to write, because they don't really have "write" permission in the head repo
|
||||
if err := pr.Issue.LoadPoster(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := pr.LoadHeadRepo(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
posterHeadPerm, err := access_model.GetIndividualUserRepoPermission(ctx, pr.HeadRepo, pr.Issue.Poster)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !posterHeadPerm.CanWrite(unit.TypeCode) {
|
||||
continue
|
||||
}
|
||||
|
||||
// check the doer's permission
|
||||
// Only allow the doer to edit the PR if they have write access to the base repository
|
||||
if err := pr.LoadBaseRepo(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
doerBasePerm, err := access_model.GetIndividualUserRepoPermission(ctx, pr.BaseRepo, doer)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if doerBasePerm.CanWrite(unit.TypeCode) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// HasUnmergedPullRequestsByHeadInfo checks if there are open and not merged pull request
|
||||
@@ -324,12 +356,26 @@ func (prs PullRequestList) LoadReviews(ctx context.Context) (ReviewList, error)
|
||||
|
||||
// HasMergedPullRequestInRepo returns whether the user(poster) has merged pull-request in the repo
|
||||
func HasMergedPullRequestInRepo(ctx context.Context, repoID, posterID int64) (bool, error) {
|
||||
return db.GetEngine(ctx).
|
||||
return HasMergedPullRequestInRepoBefore(ctx, repoID, posterID, 0, 0)
|
||||
}
|
||||
|
||||
// HasMergedPullRequestInRepoBefore returns whether the user has a merged PR before a timestamp (0 = no limit)
|
||||
func HasMergedPullRequestInRepoBefore(ctx context.Context, repoID, posterID int64, beforeUnix timeutil.TimeStamp, excludePullID int64) (bool, error) {
|
||||
sess := db.GetEngine(ctx).
|
||||
Join("INNER", "pull_request", "pull_request.issue_id = issue.id").
|
||||
Where("repo_id=?", repoID).
|
||||
And("poster_id=?", posterID).
|
||||
And("is_pull=?", true).
|
||||
And("pull_request.has_merged=?", true).
|
||||
And("pull_request.has_merged=?", true)
|
||||
|
||||
if beforeUnix > 0 {
|
||||
sess.And("pull_request.merged_unix < ?", beforeUnix)
|
||||
}
|
||||
if excludePullID > 0 {
|
||||
sess.And("pull_request.id != ?", excludePullID)
|
||||
}
|
||||
|
||||
return sess.
|
||||
Select("issue.id").
|
||||
Limit(1).
|
||||
Get(new(Issue))
|
||||
|
||||
@@ -6,15 +6,28 @@ package issues_test
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func TestPullRequestList_LoadAttributes(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func TestPullRequestList(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
t.Run("LoadAttributes", testPullRequestListLoadAttributes)
|
||||
t.Run("LoadReviewCommentsCounts", testPullRequestListLoadReviewCommentsCounts)
|
||||
t.Run("LoadReviews", testPullRequestListLoadReviews)
|
||||
t.Run("CanMaintainerWriteToBranch", testCanMaintainerWriteToBranch)
|
||||
}
|
||||
|
||||
func testPullRequestListLoadAttributes(t *testing.T) {
|
||||
prs := issues_model.PullRequestList{
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}),
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}),
|
||||
@@ -28,9 +41,7 @@ func TestPullRequestList_LoadAttributes(t *testing.T) {
|
||||
assert.NoError(t, issues_model.PullRequestList([]*issues_model.PullRequest{}).LoadAttributes(t.Context()))
|
||||
}
|
||||
|
||||
func TestPullRequestList_LoadReviewCommentsCounts(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
func testPullRequestListLoadReviewCommentsCounts(t *testing.T) {
|
||||
prs := issues_model.PullRequestList{
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}),
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}),
|
||||
@@ -43,9 +54,7 @@ func TestPullRequestList_LoadReviewCommentsCounts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullRequestList_LoadReviews(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
func testPullRequestListLoadReviews(t *testing.T) {
|
||||
prs := issues_model.PullRequestList{
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}),
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}),
|
||||
@@ -61,3 +70,73 @@ func TestPullRequestList_LoadReviews(t *testing.T) {
|
||||
assert.EqualValues(t, 10, reviewList[4].ID)
|
||||
assert.EqualValues(t, 22, reviewList[5].ID)
|
||||
}
|
||||
|
||||
func testCanMaintainerWriteToBranch(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
baseRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
||||
headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11})
|
||||
|
||||
_ = baseRepo.LoadOwner(ctx)
|
||||
_ = headRepo.LoadOwner(ctx)
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
// a PR from header's owner
|
||||
headOwnerPR := &issues_model.PullRequest{
|
||||
Issue: &issues_model.Issue{
|
||||
RepoID: baseRepo.ID,
|
||||
PosterID: headRepo.OwnerID,
|
||||
},
|
||||
HeadRepoID: headRepo.ID,
|
||||
BaseRepoID: baseRepo.ID,
|
||||
HeadBranch: "pr-from-head-owner",
|
||||
BaseBranch: "master",
|
||||
}
|
||||
require.NoError(t, issues_model.NewPullRequest(ctx, baseRepo, headOwnerPR.Issue, nil, nil, headOwnerPR))
|
||||
|
||||
// a PR from a user, they might have or not have "write" permission in the target repo
|
||||
anyUserPR := &issues_model.PullRequest{
|
||||
Issue: &issues_model.Issue{
|
||||
RepoID: baseRepo.ID,
|
||||
PosterID: user.ID,
|
||||
},
|
||||
HeadRepoID: headRepo.ID,
|
||||
BaseRepoID: baseRepo.ID,
|
||||
HeadBranch: "pr-from-head-user",
|
||||
BaseBranch: "master",
|
||||
}
|
||||
require.NoError(t, issues_model.NewPullRequest(ctx, baseRepo, anyUserPR.Issue, nil, nil, anyUserPR))
|
||||
|
||||
doerCanWrite := func(doer *user_model.User, pr *issues_model.PullRequest) bool {
|
||||
headPerm, _ := access.GetIndividualUserRepoPermission(ctx, headRepo, doer)
|
||||
return issues_model.CanMaintainerWriteToBranch(ctx, headPerm, pr.HeadBranch, doer)
|
||||
}
|
||||
|
||||
t.Run("NoAllowMaintainerEdit", func(t *testing.T) {
|
||||
assert.True(t, doerCanWrite(headRepo.Owner, headOwnerPR))
|
||||
assert.False(t, doerCanWrite(baseRepo.Owner, headOwnerPR))
|
||||
assert.False(t, doerCanWrite(baseRepo.Owner, anyUserPR))
|
||||
assert.False(t, doerCanWrite(user, anyUserPR))
|
||||
})
|
||||
|
||||
t.Run("WithAllowMaintainerEdit-HeadPosterReader", func(t *testing.T) {
|
||||
_, err := db.GetEngine(ctx).Where(builder.In("id", []int64{headOwnerPR.ID, anyUserPR.ID})).
|
||||
Cols("allow_maintainer_edit").
|
||||
Update(&issues_model.PullRequest{AllowMaintainerEdit: true})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, doerCanWrite(baseRepo.Owner, headOwnerPR))
|
||||
assert.False(t, doerCanWrite(baseRepo.Owner, anyUserPR)) // poster doesn't have write permission, so maintainer can't write either
|
||||
})
|
||||
|
||||
t.Run("WithAllowMaintainerEdit-HeadPosterWriter", func(t *testing.T) {
|
||||
_, err := db.GetEngine(ctx).Where(builder.In("id", []int64{headOwnerPR.ID, anyUserPR.ID})).
|
||||
Cols("allow_maintainer_edit").
|
||||
Update(&issues_model.PullRequest{AllowMaintainerEdit: true})
|
||||
require.NoError(t, err)
|
||||
err = db.Insert(ctx, &repo_model.Collaboration{RepoID: headRepo.ID, UserID: user.ID, Mode: perm.AccessModeWrite})
|
||||
require.NoError(t, err)
|
||||
err = db.Insert(ctx, &access.Access{RepoID: headRepo.ID, UserID: user.ID, Mode: perm.AccessModeWrite})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, doerCanWrite(baseRepo.Owner, headOwnerPR))
|
||||
assert.True(t, doerCanWrite(baseRepo.Owner, anyUserPR)) // now the poster has the write permission
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,16 +17,43 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPullRequest_LoadAttributes(t *testing.T) {
|
||||
func TestPullRequest(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
t.Run("LoadAttributes", testPullRequestLoadAttributes)
|
||||
t.Run("LoadIssue", testPullRequestLoadIssue)
|
||||
t.Run("LoadBaseRepo", testPullRequestLoadBaseRepo)
|
||||
t.Run("LoadHeadRepo", testPullRequestLoadHeadRepo)
|
||||
t.Run("PullRequestsNewest", testPullRequestsNewest)
|
||||
t.Run("PullRequestsOldest", testPullRequestsOldest)
|
||||
t.Run("GetUnmergedPullRequest", testGetUnmergedPullRequest)
|
||||
t.Run("HasUnmergedPullRequestsByHeadInfo", testHasUnmergedPullRequestsByHeadInfo)
|
||||
t.Run("GetUnmergedPullRequestsByHeadInfo", testGetUnmergedPullRequestsByHeadInfo)
|
||||
t.Run("GetUnmergedPullRequestsByBaseInfo", testGetUnmergedPullRequestsByBaseInfo)
|
||||
t.Run("GetPullRequestByIndex", testGetPullRequestByIndex)
|
||||
t.Run("GetPullRequestByID", testGetPullRequestByID)
|
||||
t.Run("GetPullRequestByIssueID", testGetPullRequestByIssueID)
|
||||
t.Run("PullRequest_UpdateCols", testPullRequestUpdateCols)
|
||||
t.Run("PullRequest_IsWorkInProgress", testPullRequestIsWorkInProgress)
|
||||
t.Run("PullRequest_GetWorkInProgressPrefixWorkInProgress", testPullRequestGetWorkInProgressPrefixWorkInProgress)
|
||||
t.Run("DeleteOrphanedObjects", testDeleteOrphanedObjects)
|
||||
t.Run("ParseCodeOwnersLine", testParseCodeOwnersLine)
|
||||
t.Run("CodeOwnerAbsolutePathPatterns", testCodeOwnerAbsolutePathPatterns)
|
||||
t.Run("GetApprovers", testGetApprovers)
|
||||
t.Run("GetPullRequestByMergedCommit", testGetPullRequestByMergedCommit)
|
||||
t.Run("Migrate_InsertPullRequests", testMigrateInsertPullRequests)
|
||||
t.Run("PullRequestsClosedRecentSortType", testPullRequestsClosedRecentSortType)
|
||||
t.Run("LoadRequestedReviewers", testLoadRequestedReviewers)
|
||||
}
|
||||
|
||||
func testPullRequestLoadAttributes(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
|
||||
assert.NoError(t, pr.LoadAttributes(t.Context()))
|
||||
assert.NotNil(t, pr.Merger)
|
||||
assert.Equal(t, pr.MergerID, pr.Merger.ID)
|
||||
}
|
||||
|
||||
func TestPullRequest_LoadIssue(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testPullRequestLoadIssue(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
|
||||
assert.NoError(t, pr.LoadIssue(t.Context()))
|
||||
assert.NotNil(t, pr.Issue)
|
||||
@@ -36,8 +63,7 @@ func TestPullRequest_LoadIssue(t *testing.T) {
|
||||
assert.Equal(t, int64(2), pr.Issue.ID)
|
||||
}
|
||||
|
||||
func TestPullRequest_LoadBaseRepo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testPullRequestLoadBaseRepo(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
|
||||
assert.NoError(t, pr.LoadBaseRepo(t.Context()))
|
||||
assert.NotNil(t, pr.BaseRepo)
|
||||
@@ -47,8 +73,7 @@ func TestPullRequest_LoadBaseRepo(t *testing.T) {
|
||||
assert.Equal(t, pr.BaseRepoID, pr.BaseRepo.ID)
|
||||
}
|
||||
|
||||
func TestPullRequest_LoadHeadRepo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testPullRequestLoadHeadRepo(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1})
|
||||
assert.NoError(t, pr.LoadHeadRepo(t.Context()))
|
||||
assert.NotNil(t, pr.HeadRepo)
|
||||
@@ -59,8 +84,7 @@ func TestPullRequest_LoadHeadRepo(t *testing.T) {
|
||||
|
||||
// TODO TestNewPullRequest
|
||||
|
||||
func TestPullRequestsNewest(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testPullRequestsNewest(t *testing.T) {
|
||||
prs, count, err := issues_model.PullRequests(t.Context(), 1, &issues_model.PullRequestsOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
@@ -77,7 +101,7 @@ func TestPullRequestsNewest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullRequests_Closed_RecentSortType(t *testing.T) {
|
||||
func testPullRequestsClosedRecentSortType(t *testing.T) {
|
||||
// Issue ID | Closed At. | Updated At
|
||||
// 2 | 1707270001 | 1707270001
|
||||
// 3 | 1707271000 | 1707279999
|
||||
@@ -90,7 +114,6 @@ func TestPullRequests_Closed_RecentSortType(t *testing.T) {
|
||||
{"recentclose", []int64{11, 3, 2}},
|
||||
}
|
||||
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
_, err := db.Exec(t.Context(), "UPDATE issue SET closed_unix = 1707270001, updated_unix = 1707270001, is_closed = true WHERE id = 2")
|
||||
require.NoError(t, err)
|
||||
_, err = db.Exec(t.Context(), "UPDATE issue SET closed_unix = 1707271000, updated_unix = 1707279999, is_closed = true WHERE id = 3")
|
||||
@@ -118,9 +141,7 @@ func TestPullRequests_Closed_RecentSortType(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRequestedReviewers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
func testLoadRequestedReviewers(t *testing.T) {
|
||||
pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
assert.NoError(t, pull.LoadIssue(t.Context()))
|
||||
issue := pull.Issue
|
||||
@@ -130,7 +151,7 @@ func TestLoadRequestedReviewers(t *testing.T) {
|
||||
user1, err := user_model.GetUserByID(t.Context(), 1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
comment, err := issues_model.AddReviewRequest(t.Context(), issue, user1, &user_model.User{})
|
||||
comment, err := issues_model.AddReviewRequest(t.Context(), issue, user1, &user_model.User{}, false)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, comment)
|
||||
|
||||
@@ -146,8 +167,7 @@ func TestLoadRequestedReviewers(t *testing.T) {
|
||||
assert.Empty(t, pull.RequestedReviewers)
|
||||
}
|
||||
|
||||
func TestPullRequestsOldest(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testPullRequestsOldest(t *testing.T) {
|
||||
prs, count, err := issues_model.PullRequests(t.Context(), 1, &issues_model.PullRequestsOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
@@ -164,8 +184,7 @@ func TestPullRequestsOldest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUnmergedPullRequest(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testGetUnmergedPullRequest(t *testing.T) {
|
||||
pr, err := issues_model.GetUnmergedPullRequest(t.Context(), 1, 1, "branch2", "master", issues_model.PullRequestFlowGithub)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), pr.ID)
|
||||
@@ -175,9 +194,7 @@ func TestGetUnmergedPullRequest(t *testing.T) {
|
||||
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
||||
}
|
||||
|
||||
func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
func testHasUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
||||
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(t.Context(), 1, "branch2")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exist)
|
||||
@@ -187,8 +204,7 @@ func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
||||
assert.False(t, exist)
|
||||
}
|
||||
|
||||
func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testGetUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
||||
prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(t.Context(), 1, "branch2")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, prs, 1)
|
||||
@@ -198,8 +214,7 @@ func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testGetUnmergedPullRequestsByBaseInfo(t *testing.T) {
|
||||
prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(t.Context(), 1, "master")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, prs, 1)
|
||||
@@ -209,8 +224,7 @@ func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) {
|
||||
assert.Equal(t, "master", pr.BaseBranch)
|
||||
}
|
||||
|
||||
func TestGetPullRequestByIndex(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testGetPullRequestByIndex(t *testing.T) {
|
||||
pr, err := issues_model.GetPullRequestByIndex(t.Context(), 1, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), pr.BaseRepoID)
|
||||
@@ -225,8 +239,7 @@ func TestGetPullRequestByIndex(t *testing.T) {
|
||||
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetPullRequestByID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testGetPullRequestByID(t *testing.T) {
|
||||
pr, err := issues_model.GetPullRequestByID(t.Context(), 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), pr.ID)
|
||||
@@ -237,8 +250,7 @@ func TestGetPullRequestByID(t *testing.T) {
|
||||
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetPullRequestByIssueID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testGetPullRequestByIssueID(t *testing.T) {
|
||||
pr, err := issues_model.GetPullRequestByIssueID(t.Context(), 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), pr.IssueID)
|
||||
@@ -248,8 +260,7 @@ func TestGetPullRequestByIssueID(t *testing.T) {
|
||||
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
||||
}
|
||||
|
||||
func TestPullRequest_UpdateCols(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testPullRequestUpdateCols(t *testing.T) {
|
||||
pr := &issues_model.PullRequest{
|
||||
ID: 1,
|
||||
BaseBranch: "baseBranch",
|
||||
@@ -265,9 +276,7 @@ func TestPullRequest_UpdateCols(t *testing.T) {
|
||||
|
||||
// TODO TestAddTestPullRequestTask
|
||||
|
||||
func TestPullRequest_IsWorkInProgress(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
func testPullRequestIsWorkInProgress(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
pr.LoadIssue(t.Context())
|
||||
|
||||
@@ -280,9 +289,7 @@ func TestPullRequest_IsWorkInProgress(t *testing.T) {
|
||||
assert.True(t, pr.IsWorkInProgress(t.Context()))
|
||||
}
|
||||
|
||||
func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
func testPullRequestGetWorkInProgressPrefixWorkInProgress(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
pr.LoadIssue(t.Context())
|
||||
|
||||
@@ -296,9 +303,7 @@ func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) {
|
||||
assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix(t.Context()))
|
||||
}
|
||||
|
||||
func TestDeleteOrphanedObjects(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
func testDeleteOrphanedObjects(t *testing.T) {
|
||||
countBefore, err := db.GetEngine(t.Context()).Count(&issues_model.PullRequest{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
@@ -317,7 +322,7 @@ func TestDeleteOrphanedObjects(t *testing.T) {
|
||||
assert.Equal(t, countBefore, countAfter)
|
||||
}
|
||||
|
||||
func TestParseCodeOwnersLine(t *testing.T) {
|
||||
func testParseCodeOwnersLine(t *testing.T) {
|
||||
type CodeOwnerTest struct {
|
||||
Line string
|
||||
Tokens []string
|
||||
@@ -331,6 +336,8 @@ func TestParseCodeOwnersLine(t *testing.T) {
|
||||
{Line: `docs/(aws|google|azure)/[^/]*\\.(md|txt) @org3 @org2/team2`, Tokens: []string{`docs/(aws|google|azure)/[^/]*\.(md|txt)`, "@org3", "@org2/team2"}},
|
||||
{Line: `\#path @org3`, Tokens: []string{`#path`, "@org3"}},
|
||||
{Line: `path\ with\ spaces/ @org3`, Tokens: []string{`path with spaces/`, "@org3"}},
|
||||
{Line: `/docs/.*\\.md @user1`, Tokens: []string{`/docs/.*\.md`, "@user1"}},
|
||||
{Line: `!/assets/.*\\.(bin|exe|msi) @user1`, Tokens: []string{`!/assets/.*\.(bin|exe|msi)`, "@user1"}},
|
||||
}
|
||||
|
||||
for _, g := range given {
|
||||
@@ -339,8 +346,37 @@ func TestParseCodeOwnersLine(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetApprovers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testCodeOwnerAbsolutePathPatterns(t *testing.T) {
|
||||
type testCase struct {
|
||||
content string
|
||||
file string
|
||||
expected bool
|
||||
}
|
||||
|
||||
cases := []testCase{
|
||||
// Absolute path pattern should match (leading "/" stripped)
|
||||
{content: "/README.md @user5\n", file: "README.md", expected: true},
|
||||
// Absolute path pattern in subdirectory
|
||||
{content: "/docs/.* @user5\n", file: "docs/foo.md", expected: true},
|
||||
// Absolute path should not match nested paths it shouldn't
|
||||
{content: "/docs/.* @user5\n", file: "other/docs/foo.md", expected: false},
|
||||
// Relative path still works
|
||||
{content: "README.md @user5\n", file: "README.md", expected: true},
|
||||
// Negated absolute path pattern
|
||||
{content: "!/.* @user5\n", file: "README.md", expected: false},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
rules, _ := issues_model.GetCodeOwnersFromContent(t.Context(), c.content)
|
||||
require.NotEmpty(t, rules)
|
||||
rule := rules[0]
|
||||
regexpMatched, _ := rule.Rule.MatchString(c.file)
|
||||
ruleMatched := regexpMatched == !rule.Negative
|
||||
assert.Equal(t, c.expected, ruleMatched, "pattern %q against file %q", c.content, c.file)
|
||||
}
|
||||
}
|
||||
|
||||
func testGetApprovers(t *testing.T) {
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 5})
|
||||
// Official reviews are already deduplicated. Allow unofficial reviews
|
||||
// to assert that there are no duplicated approvers.
|
||||
@@ -350,8 +386,7 @@ func TestGetApprovers(t *testing.T) {
|
||||
assert.Equal(t, expected, approvers)
|
||||
}
|
||||
|
||||
func TestGetPullRequestByMergedCommit(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testGetPullRequestByMergedCommit(t *testing.T) {
|
||||
pr, err := issues_model.GetPullRequestByMergedCommit(t.Context(), 1, "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, pr.ID)
|
||||
@@ -362,8 +397,7 @@ func TestGetPullRequestByMergedCommit(t *testing.T) {
|
||||
assert.ErrorAs(t, err, &issues_model.ErrPullRequestNotExist{})
|
||||
}
|
||||
|
||||
func TestMigrate_InsertPullRequests(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
func testMigrateInsertPullRequests(t *testing.T) {
|
||||
reponame := "repo1"
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
@@ -176,15 +176,7 @@ func (r *Review) LoadReviewer(ctx context.Context) (err error) {
|
||||
if r.ReviewerID == 0 || r.Reviewer != nil {
|
||||
return err
|
||||
}
|
||||
r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID)
|
||||
if err != nil {
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
return fmt.Errorf("GetPossibleUserByID [%d]: %w", r.ReviewerID, err)
|
||||
}
|
||||
r.ReviewerID = user_model.GhostUserID
|
||||
r.Reviewer = user_model.NewGhostUser()
|
||||
return nil
|
||||
}
|
||||
r.ReviewerID, r.Reviewer, err = user_model.GetPossibleUserByID(ctx, r.ReviewerID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -215,25 +207,25 @@ func (r *Review) LoadAttributes(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// HTMLTypeColorName returns the color used in the ui indicating the review
|
||||
func (r *Review) HTMLTypeColorName() string {
|
||||
// HTMLTypeColorClass returns the CSS class used in the ui indicating the review
|
||||
func (r *Review) HTMLTypeColorClass() string {
|
||||
switch r.Type {
|
||||
case ReviewTypeApprove:
|
||||
if !r.Official {
|
||||
return "grey"
|
||||
return "tw-text-text-light"
|
||||
}
|
||||
if r.Stale {
|
||||
return "yellow"
|
||||
return "tw-text-yellow"
|
||||
}
|
||||
return "green"
|
||||
return "tw-text-green"
|
||||
case ReviewTypeComment:
|
||||
return "grey"
|
||||
return "tw-text-text-light"
|
||||
case ReviewTypeReject:
|
||||
return "red"
|
||||
return "tw-text-red"
|
||||
case ReviewTypeRequest:
|
||||
return "yellow"
|
||||
return "tw-text-yellow"
|
||||
}
|
||||
return "grey"
|
||||
return "tw-text-text-light"
|
||||
}
|
||||
|
||||
// TooltipContent returns the locale string describing the review type
|
||||
@@ -384,7 +376,7 @@ func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error
|
||||
// GetCurrentReview returns the current pending review of reviewer for given issue
|
||||
func GetCurrentReview(ctx context.Context, reviewer *user_model.User, issue *Issue) (*Review, error) {
|
||||
if reviewer == nil {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil when reviewer is nil
|
||||
}
|
||||
reviews, err := FindReviews(ctx, FindReviewOptions{
|
||||
Types: []ReviewType{ReviewTypePending},
|
||||
@@ -643,7 +635,7 @@ func InsertReviews(ctx context.Context, reviews []*Review) error {
|
||||
}
|
||||
|
||||
// AddReviewRequest add a review request from one reviewer
|
||||
func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_model.User) (*Comment, error) {
|
||||
func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_model.User, isCodeOwners bool) (*Comment, error) {
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) {
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
@@ -702,6 +694,7 @@ func AddReviewRequest(ctx context.Context, issue *Issue, reviewer, doer *user_mo
|
||||
RemovedAssignee: false, // Use RemovedAssignee as !isRequest
|
||||
AssigneeID: reviewer.ID, // Use AssigneeID as reviewer ID
|
||||
ReviewID: review.ID,
|
||||
SpecialDoerName: util.Iif(isCodeOwners, SpecialDoerNameCodeOwners, ""),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -767,7 +760,7 @@ func restoreLatestOfficialReview(ctx context.Context, issueID, reviewerID int64)
|
||||
}
|
||||
|
||||
// AddTeamReviewRequest add a review request from one team
|
||||
func AddTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organization.Team, doer *user_model.User) (*Comment, error) {
|
||||
func AddTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organization.Team, doer *user_model.User, isCodeOwners bool) (*Comment, error) {
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*Comment, error) {
|
||||
review, err := GetTeamReviewerByIssueIDAndTeamID(ctx, issue.ID, reviewer.ID)
|
||||
if err != nil && !IsErrReviewNotExist(err) {
|
||||
@@ -812,6 +805,7 @@ func AddTeamReviewRequest(ctx context.Context, issue *Issue, reviewer *organizat
|
||||
RemovedAssignee: false, // Use RemovedAssignee as !isRequest
|
||||
AssigneeTeamID: reviewer.ID, // Use AssigneeTeamID as reviewer team ID
|
||||
ReviewID: review.ID,
|
||||
SpecialDoerName: util.Iif(isCodeOwners, SpecialDoerNameCodeOwners, ""),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CreateComment(): %w", err)
|
||||
@@ -917,7 +911,7 @@ func CanMarkConversation(ctx context.Context, issue *Issue, doer *user_model.Use
|
||||
return false, nil
|
||||
}
|
||||
if doer.ID != issue.PosterID {
|
||||
p, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
|
||||
p, err := access_model.GetDoerRepoPermission(ctx, issue.Repo, doer)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -173,9 +173,9 @@ func GetReviewsByIssueID(ctx context.Context, issueID int64) (latestReviews, mig
|
||||
reviewersMap := make(map[int64][]*Review) // key is reviewer id
|
||||
originalReviewersMap := make(map[int64][]*Review) // key is original author id
|
||||
reviewTeamsMap := make(map[int64][]*Review) // key is reviewer team id
|
||||
countedReivewTypes := []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, ReviewTypeComment}
|
||||
countedReviewTypes := []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, ReviewTypeComment}
|
||||
for _, review := range reviews {
|
||||
if review.ReviewerTeamID == 0 && slices.Contains(countedReivewTypes, review.Type) && !review.Dismissed {
|
||||
if review.ReviewerTeamID == 0 && slices.Contains(countedReviewTypes, review.Type) && !review.Dismissed {
|
||||
if review.OriginalAuthorID != 0 {
|
||||
originalReviewersMap[review.OriginalAuthorID] = append(originalReviewersMap[review.OriginalAuthorID], review)
|
||||
} else {
|
||||
|
||||
@@ -321,14 +321,28 @@ func TestAddReviewRequest(t *testing.T) {
|
||||
pull.HasMerged = false
|
||||
assert.NoError(t, pull.UpdateCols(t.Context(), "has_merged"))
|
||||
issue.IsClosed = true
|
||||
_, err = issues_model.AddReviewRequest(t.Context(), issue, reviewer, &user_model.User{})
|
||||
_, err = issues_model.AddReviewRequest(t.Context(), issue, reviewer, &user_model.User{}, false)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err))
|
||||
|
||||
pull.HasMerged = true
|
||||
assert.NoError(t, pull.UpdateCols(t.Context(), "has_merged"))
|
||||
issue.IsClosed = false
|
||||
_, err = issues_model.AddReviewRequest(t.Context(), issue, reviewer, &user_model.User{})
|
||||
_, err = issues_model.AddReviewRequest(t.Context(), issue, reviewer, &user_model.User{}, false)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, issues_model.IsErrReviewRequestOnClosedPR(err))
|
||||
|
||||
// Test CODEOWNERS review request stores metadata correctly
|
||||
pull2 := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2})
|
||||
assert.NoError(t, pull2.LoadIssue(t.Context()))
|
||||
issue2 := pull2.Issue
|
||||
assert.NoError(t, issue2.LoadRepo(t.Context()))
|
||||
reviewer2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 7})
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
comment, err := issues_model.AddReviewRequest(t.Context(), issue2, reviewer2, doer, true)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, comment)
|
||||
assert.NotNil(t, comment.CommentMetaData)
|
||||
assert.Equal(t, issues_model.SpecialDoerNameCodeOwners, comment.CommentMetaData.SpecialDoerName)
|
||||
}
|
||||
|
||||
@@ -311,13 +311,13 @@ func deleteTime(ctx context.Context, t *TrackedTime) error {
|
||||
}
|
||||
|
||||
// GetTrackedTimeByID returns raw TrackedTime without loading attributes by id
|
||||
func GetTrackedTimeByID(ctx context.Context, id int64) (*TrackedTime, error) {
|
||||
func GetTrackedTimeByID(ctx context.Context, issueID, trackedTimeID int64) (*TrackedTime, error) {
|
||||
time := new(TrackedTime)
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(time)
|
||||
has, err := db.GetEngine(ctx).ID(trackedTimeID).Where("issue_id = ?", issueID).Get(time)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, db.ErrNotExist{Resource: "tracked_time", ID: id}
|
||||
return nil, db.ErrNotExist{Resource: "tracked_time", ID: trackedTimeID}
|
||||
}
|
||||
return time, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user