feat: vendor gitea 1.16.2

This commit is contained in:
2026-06-02 08:36:01 +00:00
parent 247cd60f69
commit 54798b4615
2145 changed files with 162965 additions and 126447 deletions
+83 -19
View File
@@ -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++
+7 -14
View File
@@ -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
+26 -18
View File
@@ -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
+69 -23
View File
@@ -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
})
}
+84 -50
View File
@@ -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})
+16 -22
View File
@@ -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
}