mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-06-17 11:59:30 +00:00
Move issues related files into models/issues (#19931)
* Move access and repo permission to models/perm/access * fix test * fix git test * Move functions sequence * Some improvements per @KN4CK3R and @delvh * Move issues related code to models/issues * Move some issues related sub package * Merge * Fix test * Fix test * Fix test * Fix test * Rename some files
This commit is contained in:
parent
3708ca8e28
commit
1a9821f57a
180 changed files with 3667 additions and 3677 deletions
171
models/issues/assignees.go
Normal file
171
models/issues/assignees.go
Normal file
|
@ -0,0 +1,171 @@
|
|||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// IssueAssignees saves all issue assignees
|
||||
type IssueAssignees struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
AssigneeID int64 `xorm:"INDEX"`
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(IssueAssignees))
|
||||
}
|
||||
|
||||
// LoadAssignees load assignees of this issue.
|
||||
func (issue *Issue) LoadAssignees(ctx context.Context) (err error) {
|
||||
// Reset maybe preexisting assignees
|
||||
issue.Assignees = []*user_model.User{}
|
||||
issue.Assignee = nil
|
||||
|
||||
err = db.GetEngine(ctx).Table("`user`").
|
||||
Join("INNER", "issue_assignees", "assignee_id = `user`.id").
|
||||
Where("issue_assignees.issue_id = ?", issue.ID).
|
||||
Find(&issue.Assignees)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if we have at least one assignee and if yes put it in as `Assignee`
|
||||
if len(issue.Assignees) > 0 {
|
||||
issue.Assignee = issue.Assignees[0]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetAssigneeIDsByIssue returns the IDs of users assigned to an issue
|
||||
// but skips joining with `user` for performance reasons.
|
||||
// User permissions must be verified elsewhere if required.
|
||||
func GetAssigneeIDsByIssue(issueID int64) ([]int64, error) {
|
||||
userIDs := make([]int64, 0, 5)
|
||||
return userIDs, db.GetEngine(db.DefaultContext).Table("issue_assignees").
|
||||
Cols("assignee_id").
|
||||
Where("issue_id = ?", issueID).
|
||||
Distinct("assignee_id").
|
||||
Find(&userIDs)
|
||||
}
|
||||
|
||||
// IsUserAssignedToIssue returns true when the user is assigned to the issue
|
||||
func IsUserAssignedToIssue(ctx context.Context, issue *Issue, user *user_model.User) (isAssigned bool, err error) {
|
||||
return db.GetByBean(ctx, &IssueAssignees{IssueID: issue.ID, AssigneeID: user.ID})
|
||||
}
|
||||
|
||||
// ToggleIssueAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it.
|
||||
func ToggleIssueAssignee(issue *Issue, doer *user_model.User, assigneeID int64) (removed bool, comment *Comment, err error) {
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
removed, comment, err = toggleIssueAssignee(ctx, issue, doer, assigneeID, false)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
if err := committer.Commit(); err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
return removed, comment, nil
|
||||
}
|
||||
|
||||
func toggleIssueAssignee(ctx context.Context, issue *Issue, doer *user_model.User, assigneeID int64, isCreate bool) (removed bool, comment *Comment, err error) {
|
||||
removed, err = toggleUserAssignee(ctx, issue, assigneeID)
|
||||
if err != nil {
|
||||
return false, nil, fmt.Errorf("UpdateIssueUserByAssignee: %v", err)
|
||||
}
|
||||
|
||||
// Repo infos
|
||||
if err = issue.LoadRepo(ctx); err != nil {
|
||||
return false, nil, fmt.Errorf("loadRepo: %v", err)
|
||||
}
|
||||
|
||||
opts := &CreateCommentOptions{
|
||||
Type: CommentTypeAssignees,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
RemovedAssignee: removed,
|
||||
AssigneeID: assigneeID,
|
||||
}
|
||||
// Comment
|
||||
comment, err = CreateCommentCtx(ctx, opts)
|
||||
if err != nil {
|
||||
return false, nil, fmt.Errorf("createComment: %v", err)
|
||||
}
|
||||
|
||||
// if pull request is in the middle of creation - don't call webhook
|
||||
if isCreate {
|
||||
return removed, comment, err
|
||||
}
|
||||
|
||||
return removed, comment, nil
|
||||
}
|
||||
|
||||
// toggles user assignee state in database
|
||||
func toggleUserAssignee(ctx context.Context, issue *Issue, assigneeID int64) (removed bool, err error) {
|
||||
// Check if the user exists
|
||||
assignee, err := user_model.GetUserByIDCtx(ctx, assigneeID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Check if the submitted user is already assigned, if yes delete him otherwise add him
|
||||
found := false
|
||||
i := 0
|
||||
for ; i < len(issue.Assignees); i++ {
|
||||
if issue.Assignees[i].ID == assigneeID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assigneeIn := IssueAssignees{AssigneeID: assigneeID, IssueID: issue.ID}
|
||||
if found {
|
||||
issue.Assignees = append(issue.Assignees[:i], issue.Assignees[i+1:]...)
|
||||
_, err = db.DeleteByBean(ctx, &assigneeIn)
|
||||
if err != nil {
|
||||
return found, err
|
||||
}
|
||||
} else {
|
||||
issue.Assignees = append(issue.Assignees, assignee)
|
||||
if err = db.Insert(ctx, &assigneeIn); err != nil {
|
||||
return found, err
|
||||
}
|
||||
}
|
||||
|
||||
return found, nil
|
||||
}
|
||||
|
||||
// MakeIDsFromAPIAssigneesToAdd returns an array with all assignee IDs
|
||||
func MakeIDsFromAPIAssigneesToAdd(oneAssignee string, multipleAssignees []string) (assigneeIDs []int64, err error) {
|
||||
var requestAssignees []string
|
||||
|
||||
// Keeping the old assigning method for compatibility reasons
|
||||
if oneAssignee != "" && !util.IsStringInSlice(oneAssignee, multipleAssignees) {
|
||||
requestAssignees = append(requestAssignees, oneAssignee)
|
||||
}
|
||||
|
||||
// Prevent empty assignees
|
||||
if len(multipleAssignees) > 0 && multipleAssignees[0] != "" {
|
||||
requestAssignees = append(requestAssignees, multipleAssignees...)
|
||||
}
|
||||
|
||||
// Get the IDs of all assignees
|
||||
assigneeIDs, err = user_model.GetUserIDsByNames(requestAssignees, false)
|
||||
|
||||
return
|
||||
}
|
92
models/issues/assignees_test.go
Normal file
92
models/issues/assignees_test.go
Normal file
|
@ -0,0 +1,92 @@
|
|||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUpdateAssignee(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// Fake issue with assignees
|
||||
issue, err := issues_model.GetIssueWithAttrsByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Assign multiple users
|
||||
user2, err := user_model.GetUserByID(2)
|
||||
assert.NoError(t, err)
|
||||
_, _, err = issues_model.ToggleIssueAssignee(issue, &user_model.User{ID: 1}, user2.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
user3, err := user_model.GetUserByID(3)
|
||||
assert.NoError(t, err)
|
||||
_, _, err = issues_model.ToggleIssueAssignee(issue, &user_model.User{ID: 1}, user3.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
user1, err := user_model.GetUserByID(1) // This user is already assigned (see the definition in fixtures), so running UpdateAssignee should unassign him
|
||||
assert.NoError(t, err)
|
||||
_, _, err = issues_model.ToggleIssueAssignee(issue, &user_model.User{ID: 1}, user1.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check if he got removed
|
||||
isAssigned, err := issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, user1)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isAssigned)
|
||||
|
||||
// Check if they're all there
|
||||
err = issue.LoadAssignees(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var expectedAssignees []*user_model.User
|
||||
expectedAssignees = append(expectedAssignees, user2, user3)
|
||||
|
||||
for in, assignee := range issue.Assignees {
|
||||
assert.Equal(t, assignee.ID, expectedAssignees[in].ID)
|
||||
}
|
||||
|
||||
// Check if the user is assigned
|
||||
isAssigned, err = issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, user2)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, isAssigned)
|
||||
|
||||
// This user should not be assigned
|
||||
isAssigned, err = issues_model.IsUserAssignedToIssue(db.DefaultContext, issue, &user_model.User{ID: 4})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, isAssigned)
|
||||
}
|
||||
|
||||
func TestMakeIDsFromAPIAssigneesToAdd(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
_ = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
|
||||
_ = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
|
||||
|
||||
IDs, err := issues_model.MakeIDsFromAPIAssigneesToAdd("", []string{""})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int64{}, IDs)
|
||||
|
||||
_, err = issues_model.MakeIDsFromAPIAssigneesToAdd("", []string{"none_existing_user"})
|
||||
assert.Error(t, err)
|
||||
|
||||
IDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd("user1", []string{"user1"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int64{1}, IDs)
|
||||
|
||||
IDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd("user2", []string{""})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int64{2}, IDs)
|
||||
|
||||
IDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd("", []string{"user1", "user2"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int64{1, 2}, IDs)
|
||||
}
|
1546
models/issues/comment.go
Normal file
1546
models/issues/comment.go
Normal file
File diff suppressed because it is too large
Load diff
553
models/issues/comment_list.go
Normal file
553
models/issues/comment_list.go
Normal file
|
@ -0,0 +1,553 @@
|
|||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
)
|
||||
|
||||
// CommentList defines a list of comments
|
||||
type CommentList []*Comment
|
||||
|
||||
func (comments CommentList) getPosterIDs() []int64 {
|
||||
posterIDs := make(map[int64]struct{}, len(comments))
|
||||
for _, comment := range comments {
|
||||
if _, ok := posterIDs[comment.PosterID]; !ok {
|
||||
posterIDs[comment.PosterID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return container.KeysInt64(posterIDs)
|
||||
}
|
||||
|
||||
func (comments CommentList) loadPosters(ctx context.Context) error {
|
||||
if len(comments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
posterIDs := comments.getPosterIDs()
|
||||
posterMaps := make(map[int64]*user_model.User, len(posterIDs))
|
||||
left := len(posterIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
err := db.GetEngine(ctx).
|
||||
In("id", posterIDs[:limit]).
|
||||
Find(&posterMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
left -= limit
|
||||
posterIDs = posterIDs[limit:]
|
||||
}
|
||||
|
||||
for _, comment := range comments {
|
||||
if comment.PosterID <= 0 {
|
||||
continue
|
||||
}
|
||||
var ok bool
|
||||
if comment.Poster, ok = posterMaps[comment.PosterID]; !ok {
|
||||
comment.Poster = user_model.NewGhostUser()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (comments CommentList) getCommentIDs() []int64 {
|
||||
ids := make([]int64, 0, len(comments))
|
||||
for _, comment := range comments {
|
||||
ids = append(ids, comment.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (comments CommentList) getLabelIDs() []int64 {
|
||||
ids := make(map[int64]struct{}, len(comments))
|
||||
for _, comment := range comments {
|
||||
if _, ok := ids[comment.LabelID]; !ok {
|
||||
ids[comment.LabelID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return container.KeysInt64(ids)
|
||||
}
|
||||
|
||||
func (comments CommentList) loadLabels(ctx context.Context) error { //nolint
|
||||
if len(comments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
labelIDs := comments.getLabelIDs()
|
||||
commentLabels := make(map[int64]*Label, len(labelIDs))
|
||||
left := len(labelIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
rows, err := db.GetEngine(ctx).
|
||||
In("id", labelIDs[:limit]).
|
||||
Rows(new(Label))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var label Label
|
||||
err = rows.Scan(&label)
|
||||
if err != nil {
|
||||
_ = rows.Close()
|
||||
return err
|
||||
}
|
||||
commentLabels[label.ID] = &label
|
||||
}
|
||||
_ = rows.Close()
|
||||
left -= limit
|
||||
labelIDs = labelIDs[limit:]
|
||||
}
|
||||
|
||||
for _, comment := range comments {
|
||||
comment.Label = commentLabels[comment.ID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (comments CommentList) getMilestoneIDs() []int64 {
|
||||
ids := make(map[int64]struct{}, len(comments))
|
||||
for _, comment := range comments {
|
||||
if _, ok := ids[comment.MilestoneID]; !ok {
|
||||
ids[comment.MilestoneID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return container.KeysInt64(ids)
|
||||
}
|
||||
|
||||
func (comments CommentList) loadMilestones(ctx context.Context) error {
|
||||
if len(comments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
milestoneIDs := comments.getMilestoneIDs()
|
||||
if len(milestoneIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs))
|
||||
left := len(milestoneIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
err := db.GetEngine(ctx).
|
||||
In("id", milestoneIDs[:limit]).
|
||||
Find(&milestoneMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
left -= limit
|
||||
milestoneIDs = milestoneIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range comments {
|
||||
issue.Milestone = milestoneMaps[issue.MilestoneID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (comments CommentList) getOldMilestoneIDs() []int64 {
|
||||
ids := make(map[int64]struct{}, len(comments))
|
||||
for _, comment := range comments {
|
||||
if _, ok := ids[comment.OldMilestoneID]; !ok {
|
||||
ids[comment.OldMilestoneID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return container.KeysInt64(ids)
|
||||
}
|
||||
|
||||
func (comments CommentList) loadOldMilestones(ctx context.Context) error {
|
||||
if len(comments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
milestoneIDs := comments.getOldMilestoneIDs()
|
||||
if len(milestoneIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs))
|
||||
left := len(milestoneIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
err := db.GetEngine(ctx).
|
||||
In("id", milestoneIDs[:limit]).
|
||||
Find(&milestoneMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
left -= limit
|
||||
milestoneIDs = milestoneIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range comments {
|
||||
issue.OldMilestone = milestoneMaps[issue.MilestoneID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (comments CommentList) getAssigneeIDs() []int64 {
|
||||
ids := make(map[int64]struct{}, len(comments))
|
||||
for _, comment := range comments {
|
||||
if _, ok := ids[comment.AssigneeID]; !ok {
|
||||
ids[comment.AssigneeID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return container.KeysInt64(ids)
|
||||
}
|
||||
|
||||
func (comments CommentList) loadAssignees(ctx context.Context) error {
|
||||
if len(comments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
assigneeIDs := comments.getAssigneeIDs()
|
||||
assignees := make(map[int64]*user_model.User, len(assigneeIDs))
|
||||
left := len(assigneeIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
rows, err := db.GetEngine(ctx).
|
||||
In("id", assigneeIDs[:limit]).
|
||||
Rows(new(user_model.User))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var user user_model.User
|
||||
err = rows.Scan(&user)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
assignees[user.ID] = &user
|
||||
}
|
||||
_ = rows.Close()
|
||||
|
||||
left -= limit
|
||||
assigneeIDs = assigneeIDs[limit:]
|
||||
}
|
||||
|
||||
for _, comment := range comments {
|
||||
comment.Assignee = assignees[comment.AssigneeID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getIssueIDs returns all the issue ids on this comment list which issue hasn't been loaded
|
||||
func (comments CommentList) getIssueIDs() []int64 {
|
||||
ids := make(map[int64]struct{}, len(comments))
|
||||
for _, comment := range comments {
|
||||
if comment.Issue != nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := ids[comment.IssueID]; !ok {
|
||||
ids[comment.IssueID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return container.KeysInt64(ids)
|
||||
}
|
||||
|
||||
// Issues returns all the issues of comments
|
||||
func (comments CommentList) Issues() IssueList {
|
||||
issues := make(map[int64]*Issue, len(comments))
|
||||
for _, comment := range comments {
|
||||
if comment.Issue != nil {
|
||||
if _, ok := issues[comment.Issue.ID]; !ok {
|
||||
issues[comment.Issue.ID] = comment.Issue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
issueList := make([]*Issue, 0, len(issues))
|
||||
for _, issue := range issues {
|
||||
issueList = append(issueList, issue)
|
||||
}
|
||||
return issueList
|
||||
}
|
||||
|
||||
func (comments CommentList) loadIssues(ctx context.Context) error {
|
||||
if len(comments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
issueIDs := comments.getIssueIDs()
|
||||
issues := make(map[int64]*Issue, len(issueIDs))
|
||||
left := len(issueIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
rows, err := db.GetEngine(ctx).
|
||||
In("id", issueIDs[:limit]).
|
||||
Rows(new(Issue))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var issue Issue
|
||||
err = rows.Scan(&issue)
|
||||
if err != nil {
|
||||
rows.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
issues[issue.ID] = &issue
|
||||
}
|
||||
_ = rows.Close()
|
||||
|
||||
left -= limit
|
||||
issueIDs = issueIDs[limit:]
|
||||
}
|
||||
|
||||
for _, comment := range comments {
|
||||
if comment.Issue == nil {
|
||||
comment.Issue = issues[comment.IssueID]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (comments CommentList) getDependentIssueIDs() []int64 {
|
||||
ids := make(map[int64]struct{}, len(comments))
|
||||
for _, comment := range comments {
|
||||
if comment.DependentIssue != nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := ids[comment.DependentIssueID]; !ok {
|
||||
ids[comment.DependentIssueID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return container.KeysInt64(ids)
|
||||
}
|
||||
|
||||
func (comments CommentList) loadDependentIssues(ctx context.Context) error {
|
||||
if len(comments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
e := db.GetEngine(ctx)
|
||||
issueIDs := comments.getDependentIssueIDs()
|
||||
issues := make(map[int64]*Issue, len(issueIDs))
|
||||
left := len(issueIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
rows, err := e.
|
||||
In("id", issueIDs[:limit]).
|
||||
Rows(new(Issue))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var issue Issue
|
||||
err = rows.Scan(&issue)
|
||||
if err != nil {
|
||||
_ = rows.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
issues[issue.ID] = &issue
|
||||
}
|
||||
_ = rows.Close()
|
||||
|
||||
left -= limit
|
||||
issueIDs = issueIDs[limit:]
|
||||
}
|
||||
|
||||
for _, comment := range comments {
|
||||
if comment.DependentIssue == nil {
|
||||
comment.DependentIssue = issues[comment.DependentIssueID]
|
||||
if comment.DependentIssue != nil {
|
||||
if err := comment.DependentIssue.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (comments CommentList) loadAttachments(ctx context.Context) (err error) {
|
||||
if len(comments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
attachments := make(map[int64][]*repo_model.Attachment, len(comments))
|
||||
commentsIDs := comments.getCommentIDs()
|
||||
left := len(commentsIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
rows, err := db.GetEngine(ctx).Table("attachment").
|
||||
Join("INNER", "comment", "comment.id = attachment.comment_id").
|
||||
In("comment.id", commentsIDs[:limit]).
|
||||
Rows(new(repo_model.Attachment))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var attachment repo_model.Attachment
|
||||
err = rows.Scan(&attachment)
|
||||
if err != nil {
|
||||
_ = rows.Close()
|
||||
return err
|
||||
}
|
||||
attachments[attachment.CommentID] = append(attachments[attachment.CommentID], &attachment)
|
||||
}
|
||||
|
||||
_ = rows.Close()
|
||||
left -= limit
|
||||
commentsIDs = commentsIDs[limit:]
|
||||
}
|
||||
|
||||
for _, comment := range comments {
|
||||
comment.Attachments = attachments[comment.ID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (comments CommentList) getReviewIDs() []int64 {
|
||||
ids := make(map[int64]struct{}, len(comments))
|
||||
for _, comment := range comments {
|
||||
if _, ok := ids[comment.ReviewID]; !ok {
|
||||
ids[comment.ReviewID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return container.KeysInt64(ids)
|
||||
}
|
||||
|
||||
func (comments CommentList) loadReviews(ctx context.Context) error { //nolint
|
||||
if len(comments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
reviewIDs := comments.getReviewIDs()
|
||||
reviews := make(map[int64]*Review, len(reviewIDs))
|
||||
left := len(reviewIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
rows, err := db.GetEngine(ctx).
|
||||
In("id", reviewIDs[:limit]).
|
||||
Rows(new(Review))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var review Review
|
||||
err = rows.Scan(&review)
|
||||
if err != nil {
|
||||
_ = rows.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
reviews[review.ID] = &review
|
||||
}
|
||||
_ = rows.Close()
|
||||
|
||||
left -= limit
|
||||
reviewIDs = reviewIDs[limit:]
|
||||
}
|
||||
|
||||
for _, comment := range comments {
|
||||
comment.Review = reviews[comment.ReviewID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadAttributes loads all attributes
|
||||
func (comments CommentList) loadAttributes(ctx context.Context) (err error) {
|
||||
if err = comments.loadPosters(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = comments.loadLabels(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = comments.loadMilestones(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = comments.loadOldMilestones(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = comments.loadAssignees(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = comments.loadAttachments(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = comments.loadReviews(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = comments.loadIssues(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = comments.loadDependentIssues(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAttributes loads attributes of the comments, except for attachments and
|
||||
// comments
|
||||
func (comments CommentList) LoadAttributes() error {
|
||||
return comments.loadAttributes(db.DefaultContext)
|
||||
}
|
||||
|
||||
// LoadAttachments loads attachments
|
||||
func (comments CommentList) LoadAttachments() error {
|
||||
return comments.loadAttachments(db.DefaultContext)
|
||||
}
|
||||
|
||||
// LoadPosters loads posters
|
||||
func (comments CommentList) LoadPosters() error {
|
||||
return comments.loadPosters(db.DefaultContext)
|
||||
}
|
||||
|
||||
// LoadIssues loads issues of comments
|
||||
func (comments CommentList) LoadIssues() error {
|
||||
return comments.loadIssues(db.DefaultContext)
|
||||
}
|
65
models/issues/comment_test.go
Normal file
65
models/issues/comment_test.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
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"
|
||||
)
|
||||
|
||||
func TestCreateComment(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{}).(*issues_model.Issue)
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository)
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
|
||||
|
||||
now := time.Now().Unix()
|
||||
comment, err := issues_model.CreateComment(&issues_model.CreateCommentOptions{
|
||||
Type: issues_model.CommentTypeComment,
|
||||
Doer: doer,
|
||||
Repo: repo,
|
||||
Issue: issue,
|
||||
Content: "Hello",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
then := time.Now().Unix()
|
||||
|
||||
assert.EqualValues(t, issues_model.CommentTypeComment, comment.Type)
|
||||
assert.EqualValues(t, "Hello", comment.Content)
|
||||
assert.EqualValues(t, issue.ID, comment.IssueID)
|
||||
assert.EqualValues(t, doer.ID, comment.PosterID)
|
||||
unittest.AssertInt64InRange(t, now, then, int64(comment.CreatedUnix))
|
||||
unittest.AssertExistsAndLoadBean(t, comment) // assert actually added to DB
|
||||
|
||||
updatedIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID}).(*issues_model.Issue)
|
||||
unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
|
||||
}
|
||||
|
||||
func TestFetchCodeComments(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue)
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
|
||||
res, err := issues_model.FetchCodeComments(db.DefaultContext, issue, user)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, res, "README.md")
|
||||
assert.Contains(t, res["README.md"], int64(4))
|
||||
assert.Len(t, res["README.md"][4], 1)
|
||||
assert.Equal(t, int64(4), res["README.md"][4][0].ID)
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
|
||||
res, err = issues_model.FetchCodeComments(db.DefaultContext, issue, user2)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, res, 1)
|
||||
}
|
|
@ -53,13 +53,13 @@ func SaveIssueContentHistory(ctx context.Context, posterID, issueID, commentID i
|
|||
}
|
||||
// We only keep at most 20 history revisions now. It is enough in most cases.
|
||||
// If there is a special requirement to keep more, we can consider introducing a new setting option then, but not now.
|
||||
keepLimitedContentHistory(ctx, issueID, commentID, 20)
|
||||
KeepLimitedContentHistory(ctx, issueID, commentID, 20)
|
||||
return nil
|
||||
}
|
||||
|
||||
// keepLimitedContentHistory keeps at most `limit` history revisions, it will hard delete out-dated revisions, sorting by revision interval
|
||||
// KeepLimitedContentHistory keeps at most `limit` history revisions, it will hard delete out-dated revisions, sorting by revision interval
|
||||
// we can ignore all errors in this function, so we just log them
|
||||
func keepLimitedContentHistory(ctx context.Context, issueID, commentID int64, limit int) {
|
||||
func KeepLimitedContentHistory(ctx context.Context, issueID, commentID int64, limit int) {
|
||||
type IDEditTime struct {
|
||||
ID int64
|
||||
EditedUnix timeutil.TimeStamp
|
||||
|
|
|
@ -2,12 +2,13 @@
|
|||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues
|
||||
package issues_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
|
@ -20,20 +21,20 @@ func TestContentHistory(t *testing.T) {
|
|||
dbCtx := db.DefaultContext
|
||||
timeStampNow := timeutil.TimeStampNow()
|
||||
|
||||
_ = SaveIssueContentHistory(dbCtx, 1, 10, 0, timeStampNow, "i-a", true)
|
||||
_ = SaveIssueContentHistory(dbCtx, 1, 10, 0, timeStampNow.Add(2), "i-b", false)
|
||||
_ = SaveIssueContentHistory(dbCtx, 1, 10, 0, timeStampNow.Add(7), "i-c", false)
|
||||
_ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 0, timeStampNow, "i-a", true)
|
||||
_ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 0, timeStampNow.Add(2), "i-b", false)
|
||||
_ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 0, timeStampNow.Add(7), "i-c", false)
|
||||
|
||||
_ = SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow, "c-a", true)
|
||||
_ = SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(5), "c-b", false)
|
||||
_ = SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(20), "c-c", false)
|
||||
_ = SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(50), "c-d", false)
|
||||
_ = SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(51), "c-e", false)
|
||||
_ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow, "c-a", true)
|
||||
_ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(5), "c-b", false)
|
||||
_ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(20), "c-c", false)
|
||||
_ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(50), "c-d", false)
|
||||
_ = issues_model.SaveIssueContentHistory(dbCtx, 1, 10, 100, timeStampNow.Add(51), "c-e", false)
|
||||
|
||||
h1, _ := GetIssueContentHistoryByID(dbCtx, 1)
|
||||
h1, _ := issues_model.GetIssueContentHistoryByID(dbCtx, 1)
|
||||
assert.EqualValues(t, 1, h1.ID)
|
||||
|
||||
m, _ := QueryIssueContentHistoryEditedCountMap(dbCtx, 10)
|
||||
m, _ := issues_model.QueryIssueContentHistoryEditedCountMap(dbCtx, 10)
|
||||
assert.Equal(t, 3, m[0])
|
||||
assert.Equal(t, 5, m[100])
|
||||
|
||||
|
@ -48,31 +49,31 @@ func TestContentHistory(t *testing.T) {
|
|||
}
|
||||
_ = db.GetEngine(dbCtx).Sync2(&User{})
|
||||
|
||||
list1, _ := FetchIssueContentHistoryList(dbCtx, 10, 0)
|
||||
list1, _ := issues_model.FetchIssueContentHistoryList(dbCtx, 10, 0)
|
||||
assert.Len(t, list1, 3)
|
||||
list2, _ := FetchIssueContentHistoryList(dbCtx, 10, 100)
|
||||
list2, _ := issues_model.FetchIssueContentHistoryList(dbCtx, 10, 100)
|
||||
assert.Len(t, list2, 5)
|
||||
|
||||
hasHistory1, _ := HasIssueContentHistory(dbCtx, 10, 0)
|
||||
hasHistory1, _ := issues_model.HasIssueContentHistory(dbCtx, 10, 0)
|
||||
assert.True(t, hasHistory1)
|
||||
hasHistory2, _ := HasIssueContentHistory(dbCtx, 10, 1)
|
||||
hasHistory2, _ := issues_model.HasIssueContentHistory(dbCtx, 10, 1)
|
||||
assert.False(t, hasHistory2)
|
||||
|
||||
h6, h6Prev, _ := GetIssueContentHistoryAndPrev(dbCtx, 6)
|
||||
h6, h6Prev, _ := issues_model.GetIssueContentHistoryAndPrev(dbCtx, 6)
|
||||
assert.EqualValues(t, 6, h6.ID)
|
||||
assert.EqualValues(t, 5, h6Prev.ID)
|
||||
|
||||
// soft-delete
|
||||
_ = SoftDeleteIssueContentHistory(dbCtx, 5)
|
||||
h6, h6Prev, _ = GetIssueContentHistoryAndPrev(dbCtx, 6)
|
||||
_ = issues_model.SoftDeleteIssueContentHistory(dbCtx, 5)
|
||||
h6, h6Prev, _ = issues_model.GetIssueContentHistoryAndPrev(dbCtx, 6)
|
||||
assert.EqualValues(t, 6, h6.ID)
|
||||
assert.EqualValues(t, 4, h6Prev.ID)
|
||||
|
||||
// only keep 3 history revisions for comment_id=100, the first and the last should never be deleted
|
||||
keepLimitedContentHistory(dbCtx, 10, 100, 3)
|
||||
list1, _ = FetchIssueContentHistoryList(dbCtx, 10, 0)
|
||||
issues_model.KeepLimitedContentHistory(dbCtx, 10, 100, 3)
|
||||
list1, _ = issues_model.FetchIssueContentHistoryList(dbCtx, 10, 0)
|
||||
assert.Len(t, list1, 3)
|
||||
list2, _ = FetchIssueContentHistoryList(dbCtx, 10, 100)
|
||||
list2, _ = issues_model.FetchIssueContentHistoryList(dbCtx, 10, 100)
|
||||
assert.Len(t, list2, 3)
|
||||
assert.EqualValues(t, 8, list2[0].HistoryID)
|
||||
assert.EqualValues(t, 7, list2[1].HistoryID)
|
||||
|
|
210
models/issues/dependency.go
Normal file
210
models/issues/dependency.go
Normal file
|
@ -0,0 +1,210 @@
|
|||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
)
|
||||
|
||||
// ErrDependencyExists represents a "DependencyAlreadyExists" kind of error.
|
||||
type ErrDependencyExists struct {
|
||||
IssueID int64
|
||||
DependencyID int64
|
||||
}
|
||||
|
||||
// IsErrDependencyExists checks if an error is a ErrDependencyExists.
|
||||
func IsErrDependencyExists(err error) bool {
|
||||
_, ok := err.(ErrDependencyExists)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrDependencyExists) Error() string {
|
||||
return fmt.Sprintf("issue dependency does already exist [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID)
|
||||
}
|
||||
|
||||
// ErrDependencyNotExists represents a "DependencyAlreadyExists" kind of error.
|
||||
type ErrDependencyNotExists struct {
|
||||
IssueID int64
|
||||
DependencyID int64
|
||||
}
|
||||
|
||||
// IsErrDependencyNotExists checks if an error is a ErrDependencyExists.
|
||||
func IsErrDependencyNotExists(err error) bool {
|
||||
_, ok := err.(ErrDependencyNotExists)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrDependencyNotExists) Error() string {
|
||||
return fmt.Sprintf("issue dependency does not exist [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID)
|
||||
}
|
||||
|
||||
// ErrCircularDependency represents a "DependencyCircular" kind of error.
|
||||
type ErrCircularDependency struct {
|
||||
IssueID int64
|
||||
DependencyID int64
|
||||
}
|
||||
|
||||
// IsErrCircularDependency checks if an error is a ErrCircularDependency.
|
||||
func IsErrCircularDependency(err error) bool {
|
||||
_, ok := err.(ErrCircularDependency)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrCircularDependency) Error() string {
|
||||
return fmt.Sprintf("circular dependencies exists (two issues blocking each other) [issue id: %d, dependency id: %d]", err.IssueID, err.DependencyID)
|
||||
}
|
||||
|
||||
// ErrDependenciesLeft represents an error where the issue you're trying to close still has dependencies left.
|
||||
type ErrDependenciesLeft struct {
|
||||
IssueID int64
|
||||
}
|
||||
|
||||
// IsErrDependenciesLeft checks if an error is a ErrDependenciesLeft.
|
||||
func IsErrDependenciesLeft(err error) bool {
|
||||
_, ok := err.(ErrDependenciesLeft)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrDependenciesLeft) Error() string {
|
||||
return fmt.Sprintf("issue has open dependencies [issue id: %d]", err.IssueID)
|
||||
}
|
||||
|
||||
// ErrUnknownDependencyType represents an error where an unknown dependency type was passed
|
||||
type ErrUnknownDependencyType struct {
|
||||
Type DependencyType
|
||||
}
|
||||
|
||||
// IsErrUnknownDependencyType checks if an error is ErrUnknownDependencyType
|
||||
func IsErrUnknownDependencyType(err error) bool {
|
||||
_, ok := err.(ErrUnknownDependencyType)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUnknownDependencyType) Error() string {
|
||||
return fmt.Sprintf("unknown dependency type [type: %d]", err.Type)
|
||||
}
|
||||
|
||||
// IssueDependency represents an issue dependency
|
||||
type IssueDependency struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UserID int64 `xorm:"NOT NULL"`
|
||||
IssueID int64 `xorm:"UNIQUE(issue_dependency) NOT NULL"`
|
||||
DependencyID int64 `xorm:"UNIQUE(issue_dependency) NOT NULL"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(IssueDependency))
|
||||
}
|
||||
|
||||
// DependencyType Defines Dependency Type Constants
|
||||
type DependencyType int
|
||||
|
||||
// Define Dependency Types
|
||||
const (
|
||||
DependencyTypeBlockedBy DependencyType = iota
|
||||
DependencyTypeBlocking
|
||||
)
|
||||
|
||||
// CreateIssueDependency creates a new dependency for an issue
|
||||
func CreateIssueDependency(user *user_model.User, issue, dep *Issue) error {
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
// Check if it aleready exists
|
||||
exists, err := issueDepExists(ctx, issue.ID, dep.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return ErrDependencyExists{issue.ID, dep.ID}
|
||||
}
|
||||
// And if it would be circular
|
||||
circular, err := issueDepExists(ctx, dep.ID, issue.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if circular {
|
||||
return ErrCircularDependency{issue.ID, dep.ID}
|
||||
}
|
||||
|
||||
if err := db.Insert(ctx, &IssueDependency{
|
||||
UserID: user.ID,
|
||||
IssueID: issue.ID,
|
||||
DependencyID: dep.ID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add comment referencing the new dependency
|
||||
if err = createIssueDependencyComment(ctx, user, issue, dep, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// RemoveIssueDependency removes a dependency from an issue
|
||||
func RemoveIssueDependency(user *user_model.User, issue, dep *Issue, depType DependencyType) (err error) {
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
var issueDepToDelete IssueDependency
|
||||
|
||||
switch depType {
|
||||
case DependencyTypeBlockedBy:
|
||||
issueDepToDelete = IssueDependency{IssueID: issue.ID, DependencyID: dep.ID}
|
||||
case DependencyTypeBlocking:
|
||||
issueDepToDelete = IssueDependency{IssueID: dep.ID, DependencyID: issue.ID}
|
||||
default:
|
||||
return ErrUnknownDependencyType{depType}
|
||||
}
|
||||
|
||||
affected, err := db.GetEngine(ctx).Delete(&issueDepToDelete)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If we deleted nothing, the dependency did not exist
|
||||
if affected <= 0 {
|
||||
return ErrDependencyNotExists{issue.ID, dep.ID}
|
||||
}
|
||||
|
||||
// Add comment referencing the removed dependency
|
||||
if err = createIssueDependencyComment(ctx, user, issue, dep, false); err != nil {
|
||||
return err
|
||||
}
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// Check if the dependency already exists
|
||||
func issueDepExists(ctx context.Context, issueID, depID int64) (bool, error) {
|
||||
return db.GetEngine(ctx).Where("(issue_id = ? AND dependency_id = ?)", issueID, depID).Exist(&IssueDependency{})
|
||||
}
|
||||
|
||||
// IssueNoDependenciesLeft checks if issue can be closed
|
||||
func IssueNoDependenciesLeft(ctx context.Context, issue *Issue) (bool, error) {
|
||||
exists, err := db.GetEngine(ctx).
|
||||
Table("issue_dependency").
|
||||
Select("issue.*").
|
||||
Join("INNER", "issue", "issue.id = issue_dependency.dependency_id").
|
||||
Where("issue_dependency.issue_id = ?", issue.ID).
|
||||
And("issue.is_closed = ?", "0").
|
||||
Exist(&Issue{})
|
||||
|
||||
return !exists, err
|
||||
}
|
63
models/issues/dependency_test.go
Normal file
63
models/issues/dependency_test.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCreateIssueDependency(t *testing.T) {
|
||||
// Prepare
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user1, err := user_model.GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
issue2, err := issues_model.GetIssueByID(db.DefaultContext, 2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create a dependency and check if it was successful
|
||||
err = issues_model.CreateIssueDependency(user1, issue1, issue2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Do it again to see if it will check if the dependency already exists
|
||||
err = issues_model.CreateIssueDependency(user1, issue1, issue2)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, issues_model.IsErrDependencyExists(err))
|
||||
|
||||
// Check for circular dependencies
|
||||
err = issues_model.CreateIssueDependency(user1, issue2, issue1)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, issues_model.IsErrCircularDependency(err))
|
||||
|
||||
_ = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeAddDependency, PosterID: user1.ID, IssueID: issue1.ID})
|
||||
|
||||
// Check if dependencies left is correct
|
||||
left, err := issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, left)
|
||||
|
||||
// Close #2 and check again
|
||||
_, err = issues_model.ChangeIssueStatus(db.DefaultContext, issue2, user1, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
left, err = issues_model.IssueNoDependenciesLeft(db.DefaultContext, issue1)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, left)
|
||||
|
||||
// Test removing the dependency
|
||||
err = issues_model.RemoveIssueDependency(user1, issue1, issue2, issues_model.DependencyTypeBlockedBy)
|
||||
assert.NoError(t, err)
|
||||
}
|
2448
models/issues/issue.go
Normal file
2448
models/issues/issue.go
Normal file
File diff suppressed because it is too large
Load diff
32
models/issues/issue_index.go
Normal file
32
models/issues/issue_index.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues
|
||||
|
||||
import "code.gitea.io/gitea/models/db"
|
||||
|
||||
// RecalculateIssueIndexForRepo create issue_index for repo if not exist and
|
||||
// update it based on highest index of existing issues assigned to a repo
|
||||
func RecalculateIssueIndexForRepo(repoID int64) error {
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if err := db.UpsertResourceIndex(ctx, "issue_index", repoID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var max int64
|
||||
if _, err := db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := db.GetEngine(ctx).Exec("UPDATE `issue_index` SET max_index=? WHERE group_id=?", max, repoID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
565
models/issues/issue_list.go
Normal file
565
models/issues/issue_list.go
Normal file
|
@ -0,0 +1,565 @@
|
|||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// IssueList defines a list of issues
|
||||
type IssueList []*Issue
|
||||
|
||||
// get the repo IDs to be loaded later, these IDs are for issue.Repo and issue.PullRequest.HeadRepo
|
||||
func (issues IssueList) getRepoIDs() []int64 {
|
||||
repoIDs := make(map[int64]struct{}, len(issues))
|
||||
for _, issue := range issues {
|
||||
if issue.Repo == nil {
|
||||
repoIDs[issue.RepoID] = struct{}{}
|
||||
}
|
||||
if issue.PullRequest != nil && issue.PullRequest.HeadRepo == nil {
|
||||
repoIDs[issue.PullRequest.HeadRepoID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return container.KeysInt64(repoIDs)
|
||||
}
|
||||
|
||||
func (issues IssueList) loadRepositories(ctx context.Context) ([]*repo_model.Repository, error) {
|
||||
if len(issues) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
repoIDs := issues.getRepoIDs()
|
||||
repoMaps := make(map[int64]*repo_model.Repository, len(repoIDs))
|
||||
left := len(repoIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
err := db.GetEngine(ctx).
|
||||
In("id", repoIDs[:limit]).
|
||||
Find(&repoMaps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find repository: %v", err)
|
||||
}
|
||||
left -= limit
|
||||
repoIDs = repoIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
if issue.Repo == nil {
|
||||
issue.Repo = repoMaps[issue.RepoID]
|
||||
} else {
|
||||
repoMaps[issue.RepoID] = issue.Repo
|
||||
}
|
||||
if issue.PullRequest != nil {
|
||||
issue.PullRequest.BaseRepo = issue.Repo
|
||||
if issue.PullRequest.HeadRepo == nil {
|
||||
issue.PullRequest.HeadRepo = repoMaps[issue.PullRequest.HeadRepoID]
|
||||
}
|
||||
}
|
||||
}
|
||||
return repo_model.ValuesRepository(repoMaps), nil
|
||||
}
|
||||
|
||||
// LoadRepositories loads issues' all repositories
|
||||
func (issues IssueList) LoadRepositories() ([]*repo_model.Repository, error) {
|
||||
return issues.loadRepositories(db.DefaultContext)
|
||||
}
|
||||
|
||||
func (issues IssueList) getPosterIDs() []int64 {
|
||||
posterIDs := make(map[int64]struct{}, len(issues))
|
||||
for _, issue := range issues {
|
||||
if _, ok := posterIDs[issue.PosterID]; !ok {
|
||||
posterIDs[issue.PosterID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return container.KeysInt64(posterIDs)
|
||||
}
|
||||
|
||||
func (issues IssueList) loadPosters(ctx context.Context) error {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
posterIDs := issues.getPosterIDs()
|
||||
posterMaps := make(map[int64]*user_model.User, len(posterIDs))
|
||||
left := len(posterIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
err := db.GetEngine(ctx).
|
||||
In("id", posterIDs[:limit]).
|
||||
Find(&posterMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
left -= limit
|
||||
posterIDs = posterIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
if issue.PosterID <= 0 {
|
||||
continue
|
||||
}
|
||||
var ok bool
|
||||
if issue.Poster, ok = posterMaps[issue.PosterID]; !ok {
|
||||
issue.Poster = user_model.NewGhostUser()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) getIssueIDs() []int64 {
|
||||
ids := make([]int64, 0, len(issues))
|
||||
for _, issue := range issues {
|
||||
ids = append(ids, issue.ID)
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (issues IssueList) loadLabels(ctx context.Context) error {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type LabelIssue struct {
|
||||
Label *Label `xorm:"extends"`
|
||||
IssueLabel *IssueLabel `xorm:"extends"`
|
||||
}
|
||||
|
||||
issueLabels := make(map[int64][]*Label, len(issues)*3)
|
||||
issueIDs := issues.getIssueIDs()
|
||||
left := len(issueIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
rows, err := db.GetEngine(ctx).Table("label").
|
||||
Join("LEFT", "issue_label", "issue_label.label_id = label.id").
|
||||
In("issue_label.issue_id", issueIDs[:limit]).
|
||||
Asc("label.name").
|
||||
Rows(new(LabelIssue))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var labelIssue LabelIssue
|
||||
err = rows.Scan(&labelIssue)
|
||||
if err != nil {
|
||||
if err1 := rows.Close(); err1 != nil {
|
||||
return fmt.Errorf("IssueList.loadLabels: Close: %v", err1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
issueLabels[labelIssue.IssueLabel.IssueID] = append(issueLabels[labelIssue.IssueLabel.IssueID], labelIssue.Label)
|
||||
}
|
||||
// When there are no rows left and we try to close it.
|
||||
// Since that is not relevant for us, we can safely ignore it.
|
||||
if err1 := rows.Close(); err1 != nil {
|
||||
return fmt.Errorf("IssueList.loadLabels: Close: %v", err1)
|
||||
}
|
||||
left -= limit
|
||||
issueIDs = issueIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.Labels = issueLabels[issue.ID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) getMilestoneIDs() []int64 {
|
||||
ids := make(map[int64]struct{}, len(issues))
|
||||
for _, issue := range issues {
|
||||
if _, ok := ids[issue.MilestoneID]; !ok {
|
||||
ids[issue.MilestoneID] = struct{}{}
|
||||
}
|
||||
}
|
||||
return container.KeysInt64(ids)
|
||||
}
|
||||
|
||||
func (issues IssueList) loadMilestones(ctx context.Context) error {
|
||||
milestoneIDs := issues.getMilestoneIDs()
|
||||
if len(milestoneIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
milestoneMaps := make(map[int64]*Milestone, len(milestoneIDs))
|
||||
left := len(milestoneIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
err := db.GetEngine(ctx).
|
||||
In("id", milestoneIDs[:limit]).
|
||||
Find(&milestoneMaps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
left -= limit
|
||||
milestoneIDs = milestoneIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.Milestone = milestoneMaps[issue.MilestoneID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) loadAssignees(ctx context.Context) error {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type AssigneeIssue struct {
|
||||
IssueAssignee *IssueAssignees `xorm:"extends"`
|
||||
Assignee *user_model.User `xorm:"extends"`
|
||||
}
|
||||
|
||||
assignees := make(map[int64][]*user_model.User, len(issues))
|
||||
issueIDs := issues.getIssueIDs()
|
||||
left := len(issueIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
rows, err := db.GetEngine(ctx).Table("issue_assignees").
|
||||
Join("INNER", "`user`", "`user`.id = `issue_assignees`.assignee_id").
|
||||
In("`issue_assignees`.issue_id", issueIDs[:limit]).
|
||||
Rows(new(AssigneeIssue))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var assigneeIssue AssigneeIssue
|
||||
err = rows.Scan(&assigneeIssue)
|
||||
if err != nil {
|
||||
if err1 := rows.Close(); err1 != nil {
|
||||
return fmt.Errorf("IssueList.loadAssignees: Close: %v", err1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
assignees[assigneeIssue.IssueAssignee.IssueID] = append(assignees[assigneeIssue.IssueAssignee.IssueID], assigneeIssue.Assignee)
|
||||
}
|
||||
if err1 := rows.Close(); err1 != nil {
|
||||
return fmt.Errorf("IssueList.loadAssignees: Close: %v", err1)
|
||||
}
|
||||
left -= limit
|
||||
issueIDs = issueIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.Assignees = assignees[issue.ID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) getPullIssueIDs() []int64 {
|
||||
ids := make([]int64, 0, len(issues))
|
||||
for _, issue := range issues {
|
||||
if issue.IsPull && issue.PullRequest == nil {
|
||||
ids = append(ids, issue.ID)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (issues IssueList) loadPullRequests(ctx context.Context) error {
|
||||
issuesIDs := issues.getPullIssueIDs()
|
||||
if len(issuesIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
pullRequestMaps := make(map[int64]*PullRequest, len(issuesIDs))
|
||||
left := len(issuesIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
rows, err := db.GetEngine(ctx).
|
||||
In("issue_id", issuesIDs[:limit]).
|
||||
Rows(new(PullRequest))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var pr PullRequest
|
||||
err = rows.Scan(&pr)
|
||||
if err != nil {
|
||||
if err1 := rows.Close(); err1 != nil {
|
||||
return fmt.Errorf("IssueList.loadPullRequests: Close: %v", err1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
pullRequestMaps[pr.IssueID] = &pr
|
||||
}
|
||||
if err1 := rows.Close(); err1 != nil {
|
||||
return fmt.Errorf("IssueList.loadPullRequests: Close: %v", err1)
|
||||
}
|
||||
left -= limit
|
||||
issuesIDs = issuesIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.PullRequest = pullRequestMaps[issue.ID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) loadAttachments(ctx context.Context) (err error) {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
attachments := make(map[int64][]*repo_model.Attachment, len(issues))
|
||||
issuesIDs := issues.getIssueIDs()
|
||||
left := len(issuesIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
rows, err := db.GetEngine(ctx).Table("attachment").
|
||||
Join("INNER", "issue", "issue.id = attachment.issue_id").
|
||||
In("issue.id", issuesIDs[:limit]).
|
||||
Rows(new(repo_model.Attachment))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var attachment repo_model.Attachment
|
||||
err = rows.Scan(&attachment)
|
||||
if err != nil {
|
||||
if err1 := rows.Close(); err1 != nil {
|
||||
return fmt.Errorf("IssueList.loadAttachments: Close: %v", err1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
attachments[attachment.IssueID] = append(attachments[attachment.IssueID], &attachment)
|
||||
}
|
||||
if err1 := rows.Close(); err1 != nil {
|
||||
return fmt.Errorf("IssueList.loadAttachments: Close: %v", err1)
|
||||
}
|
||||
left -= limit
|
||||
issuesIDs = issuesIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.Attachments = attachments[issue.ID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) loadComments(ctx context.Context, cond builder.Cond) (err error) {
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
comments := make(map[int64][]*Comment, len(issues))
|
||||
issuesIDs := issues.getIssueIDs()
|
||||
left := len(issuesIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
rows, err := db.GetEngine(ctx).Table("comment").
|
||||
Join("INNER", "issue", "issue.id = comment.issue_id").
|
||||
In("issue.id", issuesIDs[:limit]).
|
||||
Where(cond).
|
||||
Rows(new(Comment))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var comment Comment
|
||||
err = rows.Scan(&comment)
|
||||
if err != nil {
|
||||
if err1 := rows.Close(); err1 != nil {
|
||||
return fmt.Errorf("IssueList.loadComments: Close: %v", err1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
comments[comment.IssueID] = append(comments[comment.IssueID], &comment)
|
||||
}
|
||||
if err1 := rows.Close(); err1 != nil {
|
||||
return fmt.Errorf("IssueList.loadComments: Close: %v", err1)
|
||||
}
|
||||
left -= limit
|
||||
issuesIDs = issuesIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.Comments = comments[issue.ID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issues IssueList) loadTotalTrackedTimes(ctx context.Context) (err error) {
|
||||
type totalTimesByIssue struct {
|
||||
IssueID int64
|
||||
Time int64
|
||||
}
|
||||
if len(issues) == 0 {
|
||||
return nil
|
||||
}
|
||||
trackedTimes := make(map[int64]int64, len(issues))
|
||||
|
||||
ids := make([]int64, 0, len(issues))
|
||||
for _, issue := range issues {
|
||||
if issue.Repo.IsTimetrackerEnabled() {
|
||||
ids = append(ids, issue.ID)
|
||||
}
|
||||
}
|
||||
|
||||
left := len(ids)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
|
||||
// select issue_id, sum(time) from tracked_time where issue_id in (<issue ids in current page>) group by issue_id
|
||||
rows, err := db.GetEngine(ctx).Table("tracked_time").
|
||||
Where("deleted = ?", false).
|
||||
Select("issue_id, sum(time) as time").
|
||||
In("issue_id", ids[:limit]).
|
||||
GroupBy("issue_id").
|
||||
Rows(new(totalTimesByIssue))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var totalTime totalTimesByIssue
|
||||
err = rows.Scan(&totalTime)
|
||||
if err != nil {
|
||||
if err1 := rows.Close(); err1 != nil {
|
||||
return fmt.Errorf("IssueList.loadTotalTrackedTimes: Close: %v", err1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
trackedTimes[totalTime.IssueID] = totalTime.Time
|
||||
}
|
||||
if err1 := rows.Close(); err1 != nil {
|
||||
return fmt.Errorf("IssueList.loadTotalTrackedTimes: Close: %v", err1)
|
||||
}
|
||||
left -= limit
|
||||
ids = ids[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
issue.TotalTrackedTime = trackedTimes[issue.ID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadAttributes loads all attributes, expect for attachments and comments
|
||||
func (issues IssueList) loadAttributes(ctx context.Context) error {
|
||||
if _, err := issues.loadRepositories(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadRepositories: %v", err)
|
||||
}
|
||||
|
||||
if err := issues.loadPosters(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadPosters: %v", err)
|
||||
}
|
||||
|
||||
if err := issues.loadLabels(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadLabels: %v", err)
|
||||
}
|
||||
|
||||
if err := issues.loadMilestones(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadMilestones: %v", err)
|
||||
}
|
||||
|
||||
if err := issues.loadAssignees(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadAssignees: %v", err)
|
||||
}
|
||||
|
||||
if err := issues.loadPullRequests(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadPullRequests: %v", err)
|
||||
}
|
||||
|
||||
if err := issues.loadTotalTrackedTimes(ctx); err != nil {
|
||||
return fmt.Errorf("issue.loadAttributes: loadTotalTrackedTimes: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAttributes loads attributes of the issues, except for attachments and
|
||||
// comments
|
||||
func (issues IssueList) LoadAttributes() error {
|
||||
return issues.loadAttributes(db.DefaultContext)
|
||||
}
|
||||
|
||||
// LoadAttachments loads attachments
|
||||
func (issues IssueList) LoadAttachments() error {
|
||||
return issues.loadAttachments(db.DefaultContext)
|
||||
}
|
||||
|
||||
// LoadComments loads comments
|
||||
func (issues IssueList) LoadComments() error {
|
||||
return issues.loadComments(db.DefaultContext, builder.NewCond())
|
||||
}
|
||||
|
||||
// LoadDiscussComments loads discuss comments
|
||||
func (issues IssueList) LoadDiscussComments() error {
|
||||
return issues.loadComments(db.DefaultContext, builder.Eq{"comment.type": CommentTypeComment})
|
||||
}
|
||||
|
||||
// LoadPullRequests loads pull requests
|
||||
func (issues IssueList) LoadPullRequests() error {
|
||||
return issues.loadPullRequests(db.DefaultContext)
|
||||
}
|
||||
|
||||
// GetApprovalCounts returns a map of issue ID to slice of approval counts
|
||||
// FIXME: only returns official counts due to double counting of non-official approvals
|
||||
func (issues IssueList) GetApprovalCounts(ctx context.Context) (map[int64][]*ReviewCount, error) {
|
||||
rCounts := make([]*ReviewCount, 0, 2*len(issues))
|
||||
ids := make([]int64, len(issues))
|
||||
for i, issue := range issues {
|
||||
ids[i] = issue.ID
|
||||
}
|
||||
sess := db.GetEngine(ctx).In("issue_id", ids)
|
||||
err := sess.Select("issue_id, type, count(id) as `count`").
|
||||
Where("official = ? AND dismissed = ?", true, false).
|
||||
GroupBy("issue_id, type").
|
||||
OrderBy("issue_id").
|
||||
Table("review").
|
||||
Find(&rCounts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
approvalCountMap := make(map[int64][]*ReviewCount, len(issues))
|
||||
|
||||
for _, c := range rCounts {
|
||||
approvalCountMap[c.IssueID] = append(approvalCountMap[c.IssueID], c)
|
||||
}
|
||||
|
||||
return approvalCountMap, nil
|
||||
}
|
73
models/issues/issue_list_test.go
Normal file
73
models/issues/issue_list_test.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIssueList_LoadRepositories(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
issueList := issues_model.IssueList{
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue),
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue),
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 4}).(*issues_model.Issue),
|
||||
}
|
||||
|
||||
repos, err := issueList.LoadRepositories()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, repos, 2)
|
||||
for _, issue := range issueList {
|
||||
assert.EqualValues(t, issue.RepoID, issue.Repo.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueList_LoadAttributes(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
setting.Service.EnableTimetracking = true
|
||||
issueList := issues_model.IssueList{
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue),
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 4}).(*issues_model.Issue),
|
||||
}
|
||||
|
||||
assert.NoError(t, issueList.LoadAttributes())
|
||||
for _, issue := range issueList {
|
||||
assert.EqualValues(t, issue.RepoID, issue.Repo.ID)
|
||||
for _, label := range issue.Labels {
|
||||
assert.EqualValues(t, issue.RepoID, label.RepoID)
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label.ID})
|
||||
}
|
||||
if issue.PosterID > 0 {
|
||||
assert.EqualValues(t, issue.PosterID, issue.Poster.ID)
|
||||
}
|
||||
if issue.AssigneeID > 0 {
|
||||
assert.EqualValues(t, issue.AssigneeID, issue.Assignee.ID)
|
||||
}
|
||||
if issue.MilestoneID > 0 {
|
||||
assert.EqualValues(t, issue.MilestoneID, issue.Milestone.ID)
|
||||
}
|
||||
if issue.IsPull {
|
||||
assert.EqualValues(t, issue.ID, issue.PullRequest.IssueID)
|
||||
}
|
||||
for _, attachment := range issue.Attachments {
|
||||
assert.EqualValues(t, issue.ID, attachment.IssueID)
|
||||
}
|
||||
for _, comment := range issue.Comments {
|
||||
assert.EqualValues(t, issue.ID, comment.IssueID)
|
||||
}
|
||||
if issue.ID == int64(1) {
|
||||
assert.Equal(t, int64(400), issue.TotalTrackedTime)
|
||||
} else if issue.ID == int64(2) {
|
||||
assert.Equal(t, int64(3682), issue.TotalTrackedTime)
|
||||
}
|
||||
}
|
||||
}
|
65
models/issues/issue_lock.go
Normal file
65
models/issues/issue_lock.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
)
|
||||
|
||||
// IssueLockOptions defines options for locking and/or unlocking an issue/PR
|
||||
type IssueLockOptions struct {
|
||||
Doer *user_model.User
|
||||
Issue *Issue
|
||||
Reason string
|
||||
}
|
||||
|
||||
// LockIssue locks an issue. This would limit commenting abilities to
|
||||
// users with write access to the repo
|
||||
func LockIssue(opts *IssueLockOptions) error {
|
||||
return updateIssueLock(opts, true)
|
||||
}
|
||||
|
||||
// UnlockIssue unlocks a previously locked issue.
|
||||
func UnlockIssue(opts *IssueLockOptions) error {
|
||||
return updateIssueLock(opts, false)
|
||||
}
|
||||
|
||||
func updateIssueLock(opts *IssueLockOptions, lock bool) error {
|
||||
if opts.Issue.IsLocked == lock {
|
||||
return nil
|
||||
}
|
||||
|
||||
opts.Issue.IsLocked = lock
|
||||
var commentType CommentType
|
||||
if opts.Issue.IsLocked {
|
||||
commentType = CommentTypeLock
|
||||
} else {
|
||||
commentType = CommentTypeUnlock
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if err := UpdateIssueCols(ctx, opts.Issue, "is_locked"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opt := &CreateCommentOptions{
|
||||
Doer: opts.Doer,
|
||||
Issue: opts.Issue,
|
||||
Repo: opts.Issue.Repo,
|
||||
Type: commentType,
|
||||
Content: opts.Reason,
|
||||
}
|
||||
if _, err := CreateCommentCtx(ctx, opt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
179
models/issues/issue_project.go
Normal file
179
models/issues/issue_project.go
Normal file
|
@ -0,0 +1,179 @@
|
|||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
)
|
||||
|
||||
// LoadProject load the project the issue was assigned to
|
||||
func (i *Issue) LoadProject() (err error) {
|
||||
return i.loadProject(db.DefaultContext)
|
||||
}
|
||||
|
||||
func (i *Issue) loadProject(ctx context.Context) (err error) {
|
||||
if i.Project == nil {
|
||||
var p project_model.Project
|
||||
if _, err = db.GetEngine(ctx).Table("project").
|
||||
Join("INNER", "project_issue", "project.id=project_issue.project_id").
|
||||
Where("project_issue.issue_id = ?", i.ID).
|
||||
Get(&p); err != nil {
|
||||
return err
|
||||
}
|
||||
i.Project = &p
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ProjectID return project id if issue was assigned to one
|
||||
func (i *Issue) ProjectID() int64 {
|
||||
return i.projectID(db.DefaultContext)
|
||||
}
|
||||
|
||||
func (i *Issue) projectID(ctx context.Context) int64 {
|
||||
var ip project_model.ProjectIssue
|
||||
has, err := db.GetEngine(ctx).Where("issue_id=?", i.ID).Get(&ip)
|
||||
if err != nil || !has {
|
||||
return 0
|
||||
}
|
||||
return ip.ProjectID
|
||||
}
|
||||
|
||||
// ProjectBoardID return project board id if issue was assigned to one
|
||||
func (i *Issue) ProjectBoardID() int64 {
|
||||
return i.projectBoardID(db.DefaultContext)
|
||||
}
|
||||
|
||||
func (i *Issue) projectBoardID(ctx context.Context) int64 {
|
||||
var ip project_model.ProjectIssue
|
||||
has, err := db.GetEngine(ctx).Where("issue_id=?", i.ID).Get(&ip)
|
||||
if err != nil || !has {
|
||||
return 0
|
||||
}
|
||||
return ip.ProjectBoardID
|
||||
}
|
||||
|
||||
// LoadIssuesFromBoard load issues assigned to this board
|
||||
func LoadIssuesFromBoard(b *project_model.Board) (IssueList, error) {
|
||||
issueList := make([]*Issue, 0, 10)
|
||||
|
||||
if b.ID != 0 {
|
||||
issues, err := Issues(&IssuesOptions{
|
||||
ProjectBoardID: b.ID,
|
||||
ProjectID: b.ProjectID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issueList = issues
|
||||
}
|
||||
|
||||
if b.Default {
|
||||
issues, err := Issues(&IssuesOptions{
|
||||
ProjectBoardID: -1, // Issues without ProjectBoardID
|
||||
ProjectID: b.ProjectID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issueList = append(issueList, issues...)
|
||||
}
|
||||
|
||||
if err := IssueList(issueList).LoadComments(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return issueList, nil
|
||||
}
|
||||
|
||||
// LoadIssuesFromBoardList load issues assigned to the boards
|
||||
func LoadIssuesFromBoardList(bs project_model.BoardList) (map[int64]IssueList, error) {
|
||||
issuesMap := make(map[int64]IssueList, len(bs))
|
||||
for i := range bs {
|
||||
il, err := LoadIssuesFromBoard(bs[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
issuesMap[bs[i].ID] = il
|
||||
}
|
||||
return issuesMap, nil
|
||||
}
|
||||
|
||||
// ChangeProjectAssign changes the project associated with an issue
|
||||
func ChangeProjectAssign(issue *Issue, doer *user_model.User, newProjectID int64) error {
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if err := addUpdateIssueProject(ctx, issue, doer, newProjectID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
func addUpdateIssueProject(ctx context.Context, issue *Issue, doer *user_model.User, newProjectID int64) error {
|
||||
oldProjectID := issue.projectID(ctx)
|
||||
|
||||
if _, err := db.GetEngine(ctx).Where("project_issue.issue_id=?", issue.ID).Delete(&project_model.ProjectIssue{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if oldProjectID > 0 || newProjectID > 0 {
|
||||
if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{
|
||||
Type: CommentTypeProject,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
OldProjectID: oldProjectID,
|
||||
ProjectID: newProjectID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return db.Insert(ctx, &project_model.ProjectIssue{
|
||||
IssueID: issue.ID,
|
||||
ProjectID: newProjectID,
|
||||
})
|
||||
}
|
||||
|
||||
// MoveIssueAcrossProjectBoards move a card from one board to another
|
||||
func MoveIssueAcrossProjectBoards(issue *Issue, board *project_model.Board) error {
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
var pis project_model.ProjectIssue
|
||||
has, err := sess.Where("issue_id=?", issue.ID).Get(&pis)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !has {
|
||||
return fmt.Errorf("issue has to be added to a project first")
|
||||
}
|
||||
|
||||
pis.ProjectBoardID = board.ID
|
||||
if _, err := sess.ID(pis.ID).Cols("project_board_id").Update(&pis); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
562
models/issues/issue_test.go
Normal file
562
models/issues/issue_test.go
Normal file
|
@ -0,0 +1,562 @@
|
|||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/foreignreference"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
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"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func TestIssue_ReplaceLabels(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testSuccess := func(issueID int64, labelIDs []int64) {
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueID}).(*issues_model.Issue)
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID}).(*repo_model.Repository)
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}).(*user_model.User)
|
||||
|
||||
labels := make([]*issues_model.Label, len(labelIDs))
|
||||
for i, labelID := range labelIDs {
|
||||
labels[i] = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID, RepoID: repo.ID}).(*issues_model.Label)
|
||||
}
|
||||
assert.NoError(t, issues_model.ReplaceIssueLabels(issue, labels, doer))
|
||||
unittest.AssertCount(t, &issues_model.IssueLabel{IssueID: issueID}, len(labelIDs))
|
||||
for _, labelID := range labelIDs {
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID})
|
||||
}
|
||||
}
|
||||
|
||||
testSuccess(1, []int64{2})
|
||||
testSuccess(1, []int64{1, 2})
|
||||
testSuccess(1, []int64{})
|
||||
}
|
||||
|
||||
func Test_GetIssueIDsByRepoID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
ids, err := issues_model.GetIssueIDsByRepoID(db.DefaultContext, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, ids, 5)
|
||||
}
|
||||
|
||||
func TestIssueAPIURL(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue)
|
||||
err := issue.LoadAttributes(db.DefaultContext)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/issues/1", issue.APIURL())
|
||||
}
|
||||
|
||||
func TestGetIssuesByIDs(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
testSuccess := func(expectedIssueIDs, nonExistentIssueIDs []int64) {
|
||||
issues, err := issues_model.GetIssuesByIDs(db.DefaultContext, append(expectedIssueIDs, nonExistentIssueIDs...))
|
||||
assert.NoError(t, err)
|
||||
actualIssueIDs := make([]int64, len(issues))
|
||||
for i, issue := range issues {
|
||||
actualIssueIDs[i] = issue.ID
|
||||
}
|
||||
assert.Equal(t, expectedIssueIDs, actualIssueIDs)
|
||||
}
|
||||
testSuccess([]int64{1, 2, 3}, []int64{})
|
||||
testSuccess([]int64{1, 2, 3}, []int64{unittest.NonexistentID})
|
||||
}
|
||||
|
||||
func TestGetParticipantIDsByIssue(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
checkParticipants := func(issueID int64, userIDs []int) {
|
||||
issue, err := issues_model.GetIssueByID(db.DefaultContext, issueID)
|
||||
assert.NoError(t, err)
|
||||
participants, err := issue.GetParticipantIDsByIssue(db.DefaultContext)
|
||||
if assert.NoError(t, err) {
|
||||
participantsIDs := make([]int, len(participants))
|
||||
for i, uid := range participants {
|
||||
participantsIDs[i] = int(uid)
|
||||
}
|
||||
sort.Ints(participantsIDs)
|
||||
sort.Ints(userIDs)
|
||||
assert.Equal(t, userIDs, participantsIDs)
|
||||
}
|
||||
}
|
||||
|
||||
// User 1 is issue1 poster (see fixtures/issue.yml)
|
||||
// User 2 only labeled issue1 (see fixtures/comment.yml)
|
||||
// Users 3 and 5 made actual comments (see fixtures/comment.yml)
|
||||
// User 3 is inactive, thus not active participant
|
||||
checkParticipants(1, []int{1, 5})
|
||||
}
|
||||
|
||||
func TestIssue_ClearLabels(t *testing.T) {
|
||||
tests := []struct {
|
||||
issueID int64
|
||||
doerID int64
|
||||
}{
|
||||
{1, 2}, // non-pull-request, has labels
|
||||
{2, 2}, // pull-request, has labels
|
||||
{3, 2}, // pull-request, has no labels
|
||||
}
|
||||
for _, test := range tests {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: test.issueID}).(*issues_model.Issue)
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: test.doerID}).(*user_model.User)
|
||||
assert.NoError(t, issues_model.ClearIssueLabels(issue, doer))
|
||||
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: test.issueID})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateIssueCols(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{}).(*issues_model.Issue)
|
||||
|
||||
const newTitle = "New Title for unit test"
|
||||
issue.Title = newTitle
|
||||
|
||||
prevContent := issue.Content
|
||||
issue.Content = "This should have no effect"
|
||||
|
||||
now := time.Now().Unix()
|
||||
assert.NoError(t, issues_model.UpdateIssueCols(db.DefaultContext, issue, "name"))
|
||||
then := time.Now().Unix()
|
||||
|
||||
updatedIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue.ID}).(*issues_model.Issue)
|
||||
assert.EqualValues(t, newTitle, updatedIssue.Title)
|
||||
assert.EqualValues(t, prevContent, updatedIssue.Content)
|
||||
unittest.AssertInt64InRange(t, now, then, int64(updatedIssue.UpdatedUnix))
|
||||
}
|
||||
|
||||
func TestIssues(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
for _, test := range []struct {
|
||||
Opts issues_model.IssuesOptions
|
||||
ExpectedIssueIDs []int64
|
||||
}{
|
||||
{
|
||||
issues_model.IssuesOptions{
|
||||
AssigneeID: 1,
|
||||
SortType: "oldest",
|
||||
},
|
||||
[]int64{1, 6},
|
||||
},
|
||||
{
|
||||
issues_model.IssuesOptions{
|
||||
RepoCond: builder.In("repo_id", 1, 3),
|
||||
SortType: "oldest",
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
PageSize: 4,
|
||||
},
|
||||
},
|
||||
[]int64{1, 2, 3, 5},
|
||||
},
|
||||
{
|
||||
issues_model.IssuesOptions{
|
||||
LabelIDs: []int64{1},
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
PageSize: 4,
|
||||
},
|
||||
},
|
||||
[]int64{2, 1},
|
||||
},
|
||||
{
|
||||
issues_model.IssuesOptions{
|
||||
LabelIDs: []int64{1, 2},
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
PageSize: 4,
|
||||
},
|
||||
},
|
||||
[]int64{}, // issues with **both** label 1 and 2, none of these issues matches, TODO: add more tests
|
||||
},
|
||||
} {
|
||||
issues, err := issues_model.Issues(&test.Opts)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, issues, len(test.ExpectedIssueIDs)) {
|
||||
for i, issue := range issues {
|
||||
assert.EqualValues(t, test.ExpectedIssueIDs[i], issue.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserIssueStats(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
for _, test := range []struct {
|
||||
Opts issues_model.UserIssueStatsOptions
|
||||
ExpectedIssueStats issues_model.IssueStats
|
||||
}{
|
||||
{
|
||||
issues_model.UserIssueStatsOptions{
|
||||
UserID: 1,
|
||||
RepoIDs: []int64{1},
|
||||
FilterMode: issues_model.FilterModeAll,
|
||||
},
|
||||
issues_model.IssueStats{
|
||||
YourRepositoriesCount: 1, // 6
|
||||
AssignCount: 1, // 6
|
||||
CreateCount: 1, // 6
|
||||
OpenCount: 1, // 6
|
||||
ClosedCount: 1, // 1
|
||||
},
|
||||
},
|
||||
{
|
||||
issues_model.UserIssueStatsOptions{
|
||||
UserID: 1,
|
||||
RepoIDs: []int64{1},
|
||||
FilterMode: issues_model.FilterModeAll,
|
||||
IsClosed: true,
|
||||
},
|
||||
issues_model.IssueStats{
|
||||
YourRepositoriesCount: 1, // 6
|
||||
AssignCount: 0,
|
||||
CreateCount: 0,
|
||||
OpenCount: 1, // 6
|
||||
ClosedCount: 1, // 1
|
||||
},
|
||||
},
|
||||
{
|
||||
issues_model.UserIssueStatsOptions{
|
||||
UserID: 1,
|
||||
FilterMode: issues_model.FilterModeAssign,
|
||||
},
|
||||
issues_model.IssueStats{
|
||||
YourRepositoriesCount: 1, // 6
|
||||
AssignCount: 1, // 6
|
||||
CreateCount: 1, // 6
|
||||
OpenCount: 1, // 6
|
||||
ClosedCount: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
issues_model.UserIssueStatsOptions{
|
||||
UserID: 1,
|
||||
FilterMode: issues_model.FilterModeCreate,
|
||||
},
|
||||
issues_model.IssueStats{
|
||||
YourRepositoriesCount: 1, // 6
|
||||
AssignCount: 1, // 6
|
||||
CreateCount: 1, // 6
|
||||
OpenCount: 1, // 6
|
||||
ClosedCount: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
issues_model.UserIssueStatsOptions{
|
||||
UserID: 1,
|
||||
FilterMode: issues_model.FilterModeMention,
|
||||
},
|
||||
issues_model.IssueStats{
|
||||
YourRepositoriesCount: 1, // 6
|
||||
AssignCount: 1, // 6
|
||||
CreateCount: 1, // 6
|
||||
MentionCount: 0,
|
||||
OpenCount: 0,
|
||||
ClosedCount: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
issues_model.UserIssueStatsOptions{
|
||||
UserID: 1,
|
||||
FilterMode: issues_model.FilterModeCreate,
|
||||
IssueIDs: []int64{1},
|
||||
},
|
||||
issues_model.IssueStats{
|
||||
YourRepositoriesCount: 1, // 1
|
||||
AssignCount: 1, // 1
|
||||
CreateCount: 1, // 1
|
||||
OpenCount: 1, // 1
|
||||
ClosedCount: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
issues_model.UserIssueStatsOptions{
|
||||
UserID: 2,
|
||||
Org: unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3}).(*organization.Organization),
|
||||
Team: unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 7}).(*organization.Team),
|
||||
FilterMode: issues_model.FilterModeAll,
|
||||
},
|
||||
issues_model.IssueStats{
|
||||
YourRepositoriesCount: 2,
|
||||
AssignCount: 1,
|
||||
CreateCount: 1,
|
||||
OpenCount: 2,
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("%#v", test.Opts), func(t *testing.T) {
|
||||
stats, err := issues_model.GetUserIssueStats(test.Opts)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
assert.Equal(t, test.ExpectedIssueStats, *stats)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue_loadTotalTimes(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
ms, err := issues_model.GetIssueByID(db.DefaultContext, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, ms.LoadTotalTimes(db.DefaultContext))
|
||||
assert.Equal(t, int64(3682), ms.TotalTrackedTime)
|
||||
}
|
||||
|
||||
func TestIssue_SearchIssueIDsByKeyword(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
total, ids, err := issues_model.SearchIssueIDsByKeyword(context.TODO(), "issue2", []int64{1}, 10, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, total)
|
||||
assert.EqualValues(t, []int64{2}, ids)
|
||||
|
||||
total, ids, err = issues_model.SearchIssueIDsByKeyword(context.TODO(), "first", []int64{1}, 10, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, total)
|
||||
assert.EqualValues(t, []int64{1}, ids)
|
||||
|
||||
total, ids, err = issues_model.SearchIssueIDsByKeyword(context.TODO(), "for", []int64{1}, 10, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 5, total)
|
||||
assert.ElementsMatch(t, []int64{1, 2, 3, 5, 11}, ids)
|
||||
|
||||
// issue1's comment id 2
|
||||
total, ids, err = issues_model.SearchIssueIDsByKeyword(context.TODO(), "good", []int64{1}, 10, 0)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, total)
|
||||
assert.EqualValues(t, []int64{1}, ids)
|
||||
}
|
||||
|
||||
func TestGetRepoIDsForIssuesOptions(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
|
||||
for _, test := range []struct {
|
||||
Opts issues_model.IssuesOptions
|
||||
ExpectedRepoIDs []int64
|
||||
}{
|
||||
{
|
||||
issues_model.IssuesOptions{
|
||||
AssigneeID: 2,
|
||||
},
|
||||
[]int64{3, 32},
|
||||
},
|
||||
{
|
||||
issues_model.IssuesOptions{
|
||||
RepoCond: builder.In("repo_id", 1, 2),
|
||||
},
|
||||
[]int64{1, 2},
|
||||
},
|
||||
} {
|
||||
repoIDs, err := issues_model.GetRepoIDsForIssuesOptions(&test.Opts, user)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, repoIDs, len(test.ExpectedRepoIDs)) {
|
||||
for i, repoID := range repoIDs {
|
||||
assert.EqualValues(t, test.ExpectedRepoIDs[i], repoID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testInsertIssue(t *testing.T, title, content string, expectIndex int64) *issues_model.Issue {
|
||||
var newIssue issues_model.Issue
|
||||
t.Run(title, func(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
|
||||
|
||||
issue := issues_model.Issue{
|
||||
RepoID: repo.ID,
|
||||
PosterID: user.ID,
|
||||
Poster: user,
|
||||
Title: title,
|
||||
Content: content,
|
||||
}
|
||||
err := issues_model.NewIssue(repo, &issue, nil, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
has, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Get(&newIssue)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, has)
|
||||
assert.EqualValues(t, issue.Title, newIssue.Title)
|
||||
assert.EqualValues(t, issue.Content, newIssue.Content)
|
||||
if expectIndex > 0 {
|
||||
assert.EqualValues(t, expectIndex, newIssue.Index)
|
||||
}
|
||||
})
|
||||
return &newIssue
|
||||
}
|
||||
|
||||
func TestIssue_InsertIssue(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// there are 5 issues and max index is 5 on repository 1, so this one should 6
|
||||
issue := testInsertIssue(t, "my issue1", "special issue's comments?", 6)
|
||||
_, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Delete(new(issues_model.Issue))
|
||||
assert.NoError(t, err)
|
||||
|
||||
issue = testInsertIssue(t, `my issue2, this is my son's love \n \r \ `, "special issue's '' comments?", 7)
|
||||
_, err = db.GetEngine(db.DefaultContext).ID(issue.ID).Delete(new(issues_model.Issue))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIssue_ResolveMentions(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testSuccess := func(owner, repo, doer string, mentions []string, expected []int64) {
|
||||
o := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: owner}).(*user_model.User)
|
||||
r := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: o.ID, LowerName: repo}).(*repo_model.Repository)
|
||||
issue := &issues_model.Issue{RepoID: r.ID}
|
||||
d := unittest.AssertExistsAndLoadBean(t, &user_model.User{LowerName: doer}).(*user_model.User)
|
||||
resolved, err := issues_model.ResolveIssueMentionsByVisibility(db.DefaultContext, issue, d, mentions)
|
||||
assert.NoError(t, err)
|
||||
ids := make([]int64, len(resolved))
|
||||
for i, user := range resolved {
|
||||
ids[i] = user.ID
|
||||
}
|
||||
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
||||
assert.EqualValues(t, expected, ids)
|
||||
}
|
||||
|
||||
// Public repo, existing user
|
||||
testSuccess("user2", "repo1", "user1", []string{"user5"}, []int64{5})
|
||||
// Public repo, non-existing user
|
||||
testSuccess("user2", "repo1", "user1", []string{"nonexisting"}, []int64{})
|
||||
// Public repo, doer
|
||||
testSuccess("user2", "repo1", "user1", []string{"user1"}, []int64{})
|
||||
// Private repo, team member
|
||||
testSuccess("user17", "big_test_private_4", "user20", []string{"user2"}, []int64{2})
|
||||
// Private repo, not a team member
|
||||
testSuccess("user17", "big_test_private_4", "user20", []string{"user5"}, []int64{})
|
||||
// Private repo, whole team
|
||||
testSuccess("user17", "big_test_private_4", "user15", []string{"user17/owners"}, []int64{18})
|
||||
}
|
||||
|
||||
func TestResourceIndex(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
testInsertIssue(t, fmt.Sprintf("issue %d", i+1), "my issue", 0)
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestCorrectIssueStats(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// Because the condition is to have chunked database look-ups,
|
||||
// We have to more issues than `maxQueryParameters`, we will insert.
|
||||
// maxQueryParameters + 10 issues into the testDatabase.
|
||||
// Each new issues will have a constant description "Bugs are nasty"
|
||||
// Which will be used later on.
|
||||
|
||||
issueAmount := issues_model.MaxQueryParameters + 10
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < issueAmount; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
testInsertIssue(t, fmt.Sprintf("Issue %d", i+1), "Bugs are nasty", 0)
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Now we will get all issueID's that match the "Bugs are nasty" query.
|
||||
total, ids, err := issues_model.SearchIssueIDsByKeyword(context.TODO(), "Bugs are nasty", []int64{1}, issueAmount, 0)
|
||||
|
||||
// Just to be sure.
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, issueAmount, total)
|
||||
|
||||
// Now we will call the GetIssueStats with these IDs and if working,
|
||||
// get the correct stats back.
|
||||
issueStats, err := issues_model.GetIssueStats(&issues_model.IssueStatsOptions{
|
||||
RepoID: 1,
|
||||
IssueIDs: ids,
|
||||
})
|
||||
|
||||
// Now check the values.
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, issueStats.OpenCount, issueAmount)
|
||||
}
|
||||
|
||||
func TestIssueForeignReference(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 4}).(*issues_model.Issue)
|
||||
assert.NotEqualValues(t, issue.Index, issue.ID) // make sure they are different to avoid false positive
|
||||
|
||||
// it is fine for an issue to not have a foreign reference
|
||||
err := issue.LoadAttributes(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, issue.ForeignReference)
|
||||
|
||||
var foreignIndex int64 = 12345
|
||||
_, err = issues_model.GetIssueByForeignIndex(context.Background(), issue.RepoID, foreignIndex)
|
||||
assert.True(t, foreignreference.IsErrLocalIndexNotExist(err))
|
||||
|
||||
err = db.Insert(db.DefaultContext, &foreignreference.ForeignReference{
|
||||
LocalIndex: issue.Index,
|
||||
ForeignIndex: strconv.FormatInt(foreignIndex, 10),
|
||||
RepoID: issue.RepoID,
|
||||
Type: foreignreference.TypeIssue,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = issue.LoadAttributes(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, issue.ForeignReference.ForeignIndex, strconv.FormatInt(foreignIndex, 10))
|
||||
|
||||
found, err := issues_model.GetIssueByForeignIndex(context.Background(), issue.RepoID, foreignIndex)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, found.Index, issue.Index)
|
||||
}
|
||||
|
||||
func TestMilestoneList_LoadTotalTrackedTimes(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
miles := issues_model.MilestoneList{
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone),
|
||||
}
|
||||
|
||||
assert.NoError(t, miles.LoadTotalTrackedTimes())
|
||||
|
||||
assert.Equal(t, int64(3682), miles[0].TotalTrackedTime)
|
||||
}
|
||||
|
||||
func TestLoadTotalTrackedTime(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone)
|
||||
|
||||
assert.NoError(t, milestone.LoadTotalTrackedTime())
|
||||
|
||||
assert.Equal(t, int64(3682), milestone.TotalTrackedTime)
|
||||
}
|
||||
|
||||
func TestCountIssues(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
count, err := issues_model.CountIssues(&issues_model.IssuesOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 17, count)
|
||||
}
|
87
models/issues/issue_user.go
Normal file
87
models/issues/issue_user.go
Normal file
|
@ -0,0 +1,87 @@
|
|||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
)
|
||||
|
||||
// IssueUser represents an issue-user relation.
|
||||
type IssueUser struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UID int64 `xorm:"INDEX"` // User ID.
|
||||
IssueID int64
|
||||
IsRead bool
|
||||
IsMentioned bool
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(IssueUser))
|
||||
}
|
||||
|
||||
// NewIssueUsers inserts an issue related users
|
||||
func NewIssueUsers(ctx context.Context, repo *repo_model.Repository, issue *Issue) error {
|
||||
assignees, err := repo_model.GetRepoAssignees(ctx, repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getAssignees: %v", err)
|
||||
}
|
||||
|
||||
// Poster can be anyone, append later if not one of assignees.
|
||||
isPosterAssignee := false
|
||||
|
||||
// Leave a seat for poster itself to append later, but if poster is one of assignee
|
||||
// and just waste 1 unit is cheaper than re-allocate memory once.
|
||||
issueUsers := make([]*IssueUser, 0, len(assignees)+1)
|
||||
for _, assignee := range assignees {
|
||||
issueUsers = append(issueUsers, &IssueUser{
|
||||
IssueID: issue.ID,
|
||||
UID: assignee.ID,
|
||||
})
|
||||
isPosterAssignee = isPosterAssignee || assignee.ID == issue.PosterID
|
||||
}
|
||||
if !isPosterAssignee {
|
||||
issueUsers = append(issueUsers, &IssueUser{
|
||||
IssueID: issue.ID,
|
||||
UID: issue.PosterID,
|
||||
})
|
||||
}
|
||||
|
||||
return db.Insert(ctx, issueUsers)
|
||||
}
|
||||
|
||||
// UpdateIssueUserByRead updates issue-user relation for reading.
|
||||
func UpdateIssueUserByRead(uid, issueID int64) error {
|
||||
_, err := db.GetEngine(db.DefaultContext).Exec("UPDATE `issue_user` SET is_read=? WHERE uid=? AND issue_id=?", true, uid, issueID)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateIssueUsersByMentions updates issue-user pairs by mentioning.
|
||||
func UpdateIssueUsersByMentions(ctx context.Context, issueID int64, uids []int64) error {
|
||||
for _, uid := range uids {
|
||||
iu := &IssueUser{
|
||||
UID: uid,
|
||||
IssueID: issueID,
|
||||
}
|
||||
has, err := db.GetEngine(ctx).Get(iu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
iu.IsMentioned = true
|
||||
if has {
|
||||
_, err = db.GetEngine(ctx).ID(iu.ID).Cols("is_mentioned").Update(iu)
|
||||
} else {
|
||||
_, err = db.GetEngine(ctx).Insert(iu)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
62
models/issues/issue_user_test.go
Normal file
62
models/issues/issue_user_test.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
// Copyright 2017 The Gogs Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_NewIssueUsers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
|
||||
newIssue := &issues_model.Issue{
|
||||
RepoID: repo.ID,
|
||||
PosterID: 4,
|
||||
Index: 6,
|
||||
Title: "newTestIssueTitle",
|
||||
Content: "newTestIssueContent",
|
||||
}
|
||||
|
||||
// artificially insert new issue
|
||||
unittest.AssertSuccessfulInsert(t, newIssue)
|
||||
|
||||
assert.NoError(t, issues_model.NewIssueUsers(db.DefaultContext, repo, newIssue))
|
||||
|
||||
// issue_user table should now have entries for new issue
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: newIssue.ID, UID: newIssue.PosterID})
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: newIssue.ID, UID: repo.OwnerID})
|
||||
}
|
||||
|
||||
func TestUpdateIssueUserByRead(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue)
|
||||
|
||||
assert.NoError(t, issues_model.UpdateIssueUserByRead(4, issue.ID))
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: issue.ID, UID: 4}, "is_read=1")
|
||||
|
||||
assert.NoError(t, issues_model.UpdateIssueUserByRead(4, issue.ID))
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: issue.ID, UID: 4}, "is_read=1")
|
||||
|
||||
assert.NoError(t, issues_model.UpdateIssueUserByRead(unittest.NonexistentID, unittest.NonexistentID))
|
||||
}
|
||||
|
||||
func TestUpdateIssueUsersByMentions(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue)
|
||||
|
||||
uids := []int64{2, 5}
|
||||
assert.NoError(t, issues_model.UpdateIssueUsersByMentions(db.DefaultContext, issue.ID, uids))
|
||||
for _, uid := range uids {
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueUser{IssueID: issue.ID, UID: uid}, "is_mentioned=1")
|
||||
}
|
||||
}
|
135
models/issues/issue_watch.go
Normal file
135
models/issues/issue_watch.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
)
|
||||
|
||||
// IssueWatch is connection request for receiving issue notification.
|
||||
type IssueWatch struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UserID int64 `xorm:"UNIQUE(watch) NOT NULL"`
|
||||
IssueID int64 `xorm:"UNIQUE(watch) NOT NULL"`
|
||||
IsWatching bool `xorm:"NOT NULL"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(IssueWatch))
|
||||
}
|
||||
|
||||
// IssueWatchList contains IssueWatch
|
||||
type IssueWatchList []*IssueWatch
|
||||
|
||||
// CreateOrUpdateIssueWatch set watching for a user and issue
|
||||
func CreateOrUpdateIssueWatch(userID, issueID int64, isWatching bool) error {
|
||||
iw, exists, err := GetIssueWatch(db.DefaultContext, userID, issueID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
iw = &IssueWatch{
|
||||
UserID: userID,
|
||||
IssueID: issueID,
|
||||
IsWatching: isWatching,
|
||||
}
|
||||
|
||||
if _, err := db.GetEngine(db.DefaultContext).Insert(iw); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
iw.IsWatching = isWatching
|
||||
|
||||
if _, err := db.GetEngine(db.DefaultContext).ID(iw.ID).Cols("is_watching", "updated_unix").Update(iw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIssueWatch returns all IssueWatch objects from db by user and issue
|
||||
// the current Web-UI need iw object for watchers AND explicit non-watchers
|
||||
func GetIssueWatch(ctx context.Context, userID, issueID int64) (iw *IssueWatch, exists bool, err error) {
|
||||
iw = new(IssueWatch)
|
||||
exists, err = db.GetEngine(ctx).
|
||||
Where("user_id = ?", userID).
|
||||
And("issue_id = ?", issueID).
|
||||
Get(iw)
|
||||
return
|
||||
}
|
||||
|
||||
// CheckIssueWatch check if an user is watching an issue
|
||||
// it takes participants and repo watch into account
|
||||
func CheckIssueWatch(user *user_model.User, issue *Issue) (bool, error) {
|
||||
iw, exist, err := GetIssueWatch(db.DefaultContext, user.ID, issue.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if exist {
|
||||
return iw.IsWatching, nil
|
||||
}
|
||||
w, err := repo_model.GetWatch(db.DefaultContext, user.ID, issue.RepoID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return repo_model.IsWatchMode(w.Mode) || IsUserParticipantsOfIssue(user, issue), nil
|
||||
}
|
||||
|
||||
// GetIssueWatchersIDs returns IDs of subscribers or explicit unsubscribers to a given issue id
|
||||
// but avoids joining with `user` for performance reasons
|
||||
// User permissions must be verified elsewhere if required
|
||||
func GetIssueWatchersIDs(ctx context.Context, issueID int64, watching bool) ([]int64, error) {
|
||||
ids := make([]int64, 0, 64)
|
||||
return ids, db.GetEngine(ctx).Table("issue_watch").
|
||||
Where("issue_id=?", issueID).
|
||||
And("is_watching = ?", watching).
|
||||
Select("user_id").
|
||||
Find(&ids)
|
||||
}
|
||||
|
||||
// GetIssueWatchers returns watchers/unwatchers of a given issue
|
||||
func GetIssueWatchers(ctx context.Context, issueID int64, listOptions db.ListOptions) (IssueWatchList, error) {
|
||||
sess := db.GetEngine(ctx).
|
||||
Where("`issue_watch`.issue_id = ?", issueID).
|
||||
And("`issue_watch`.is_watching = ?", true).
|
||||
And("`user`.is_active = ?", true).
|
||||
And("`user`.prohibit_login = ?", false).
|
||||
Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id")
|
||||
|
||||
if listOptions.Page != 0 {
|
||||
sess = db.SetSessionPagination(sess, &listOptions)
|
||||
watches := make([]*IssueWatch, 0, listOptions.PageSize)
|
||||
return watches, sess.Find(&watches)
|
||||
}
|
||||
watches := make([]*IssueWatch, 0, 8)
|
||||
return watches, sess.Find(&watches)
|
||||
}
|
||||
|
||||
// CountIssueWatchers count watchers/unwatchers of a given issue
|
||||
func CountIssueWatchers(ctx context.Context, issueID int64) (int64, error) {
|
||||
return db.GetEngine(ctx).
|
||||
Where("`issue_watch`.issue_id = ?", issueID).
|
||||
And("`issue_watch`.is_watching = ?", true).
|
||||
And("`user`.is_active = ?", true).
|
||||
And("`user`.prohibit_login = ?", false).
|
||||
Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id").Count(new(IssueWatch))
|
||||
}
|
||||
|
||||
// RemoveIssueWatchersByRepoID remove issue watchers by repoID
|
||||
func RemoveIssueWatchersByRepoID(ctx context.Context, userID, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).
|
||||
Join("INNER", "issue", "`issue`.id = `issue_watch`.issue_id AND `issue`.repo_id = ?", repoID).
|
||||
Where("`issue_watch`.user_id = ?", userID).
|
||||
Delete(new(IssueWatch))
|
||||
return err
|
||||
}
|
68
models/issues/issue_watch_test.go
Normal file
68
models/issues/issue_watch_test.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCreateOrUpdateIssueWatch(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
assert.NoError(t, issues_model.CreateOrUpdateIssueWatch(3, 1, true))
|
||||
iw := unittest.AssertExistsAndLoadBean(t, &issues_model.IssueWatch{UserID: 3, IssueID: 1}).(*issues_model.IssueWatch)
|
||||
assert.True(t, iw.IsWatching)
|
||||
|
||||
assert.NoError(t, issues_model.CreateOrUpdateIssueWatch(1, 1, false))
|
||||
iw = unittest.AssertExistsAndLoadBean(t, &issues_model.IssueWatch{UserID: 1, IssueID: 1}).(*issues_model.IssueWatch)
|
||||
assert.False(t, iw.IsWatching)
|
||||
}
|
||||
|
||||
func TestGetIssueWatch(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
_, exists, err := issues_model.GetIssueWatch(db.DefaultContext, 9, 1)
|
||||
assert.True(t, exists)
|
||||
assert.NoError(t, err)
|
||||
|
||||
iw, exists, err := issues_model.GetIssueWatch(db.DefaultContext, 2, 2)
|
||||
assert.True(t, exists)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, iw.IsWatching)
|
||||
|
||||
_, exists, err = issues_model.GetIssueWatch(db.DefaultContext, 3, 1)
|
||||
assert.False(t, exists)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGetIssueWatchers(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
iws, err := issues_model.GetIssueWatchers(db.DefaultContext, 1, db.ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
// Watcher is inactive, thus 0
|
||||
assert.Len(t, iws, 0)
|
||||
|
||||
iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 2, db.ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
// Watcher is explicit not watching
|
||||
assert.Len(t, iws, 0)
|
||||
|
||||
iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 5, db.ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
// Issue has no Watchers
|
||||
assert.Len(t, iws, 0)
|
||||
|
||||
iws, err = issues_model.GetIssueWatchers(db.DefaultContext, 7, db.ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
// Issue has one watcher
|
||||
assert.Len(t, iws, 1)
|
||||
}
|
357
models/issues/issue_xref.go
Normal file
357
models/issues/issue_xref.go
Normal file
|
@ -0,0 +1,357 @@
|
|||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/references"
|
||||
)
|
||||
|
||||
type crossReference struct {
|
||||
Issue *Issue
|
||||
Action references.XRefAction
|
||||
}
|
||||
|
||||
// crossReferencesContext is context to pass along findCrossReference functions
|
||||
type crossReferencesContext struct {
|
||||
Type CommentType
|
||||
Doer *user_model.User
|
||||
OrigIssue *Issue
|
||||
OrigComment *Comment
|
||||
RemoveOld bool
|
||||
}
|
||||
|
||||
func findOldCrossReferences(ctx context.Context, issueID, commentID int64) ([]*Comment, error) {
|
||||
active := make([]*Comment, 0, 10)
|
||||
return active, db.GetEngine(ctx).Where("`ref_action` IN (?, ?, ?)", references.XRefActionNone, references.XRefActionCloses, references.XRefActionReopens).
|
||||
And("`ref_issue_id` = ?", issueID).
|
||||
And("`ref_comment_id` = ?", commentID).
|
||||
Find(&active)
|
||||
}
|
||||
|
||||
func neuterCrossReferences(ctx context.Context, issueID, commentID int64) error {
|
||||
active, err := findOldCrossReferences(ctx, issueID, commentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ids := make([]int64, len(active))
|
||||
for i, c := range active {
|
||||
ids[i] = c.ID
|
||||
}
|
||||
return neuterCrossReferencesIds(ctx, ids)
|
||||
}
|
||||
|
||||
func neuterCrossReferencesIds(ctx context.Context, ids []int64) error {
|
||||
_, err := db.GetEngine(ctx).In("id", ids).Cols("`ref_action`").Update(&Comment{RefAction: references.XRefActionNeutered})
|
||||
return err
|
||||
}
|
||||
|
||||
// AddCrossReferences add cross repositories references.
|
||||
func (issue *Issue) AddCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error {
|
||||
var commentType CommentType
|
||||
if issue.IsPull {
|
||||
commentType = CommentTypePullRef
|
||||
} else {
|
||||
commentType = CommentTypeIssueRef
|
||||
}
|
||||
ctx := &crossReferencesContext{
|
||||
Type: commentType,
|
||||
Doer: doer,
|
||||
OrigIssue: issue,
|
||||
RemoveOld: removeOld,
|
||||
}
|
||||
return issue.createCrossReferences(stdCtx, ctx, issue.Title, issue.Content)
|
||||
}
|
||||
|
||||
func (issue *Issue) createCrossReferences(stdCtx context.Context, ctx *crossReferencesContext, plaincontent, mdcontent string) error {
|
||||
xreflist, err := ctx.OrigIssue.getCrossReferences(stdCtx, ctx, plaincontent, mdcontent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ctx.RemoveOld {
|
||||
var commentID int64
|
||||
if ctx.OrigComment != nil {
|
||||
commentID = ctx.OrigComment.ID
|
||||
}
|
||||
active, err := findOldCrossReferences(stdCtx, ctx.OrigIssue.ID, commentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ids := make([]int64, 0, len(active))
|
||||
for _, c := range active {
|
||||
found := false
|
||||
for i, x := range xreflist {
|
||||
if x.Issue.ID == c.IssueID && x.Action == c.RefAction {
|
||||
found = true
|
||||
xreflist = append(xreflist[:i], xreflist[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
ids = append(ids, c.ID)
|
||||
}
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
if err = neuterCrossReferencesIds(stdCtx, ids); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, xref := range xreflist {
|
||||
var refCommentID int64
|
||||
if ctx.OrigComment != nil {
|
||||
refCommentID = ctx.OrigComment.ID
|
||||
}
|
||||
opts := &CreateCommentOptions{
|
||||
Type: ctx.Type,
|
||||
Doer: ctx.Doer,
|
||||
Repo: xref.Issue.Repo,
|
||||
Issue: xref.Issue,
|
||||
RefRepoID: ctx.OrigIssue.RepoID,
|
||||
RefIssueID: ctx.OrigIssue.ID,
|
||||
RefCommentID: refCommentID,
|
||||
RefAction: xref.Action,
|
||||
RefIsPull: ctx.OrigIssue.IsPull,
|
||||
}
|
||||
_, err := CreateCommentCtx(stdCtx, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (issue *Issue) getCrossReferences(stdCtx context.Context, ctx *crossReferencesContext, plaincontent, mdcontent string) ([]*crossReference, error) {
|
||||
xreflist := make([]*crossReference, 0, 5)
|
||||
var (
|
||||
refRepo *repo_model.Repository
|
||||
refIssue *Issue
|
||||
refAction references.XRefAction
|
||||
err error
|
||||
)
|
||||
|
||||
allrefs := append(references.FindAllIssueReferences(plaincontent), references.FindAllIssueReferencesMarkdown(mdcontent)...)
|
||||
for _, ref := range allrefs {
|
||||
if ref.Owner == "" && ref.Name == "" {
|
||||
// Issues in the same repository
|
||||
if err := ctx.OrigIssue.LoadRepo(stdCtx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refRepo = ctx.OrigIssue.Repo
|
||||
} else {
|
||||
// Issues in other repositories
|
||||
refRepo, err = repo_model.GetRepositoryByOwnerAndNameCtx(stdCtx, ref.Owner, ref.Name)
|
||||
if err != nil {
|
||||
if repo_model.IsErrRepoNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if refIssue, refAction, err = ctx.OrigIssue.verifyReferencedIssue(stdCtx, ctx, refRepo, ref); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if refIssue != nil {
|
||||
xreflist = ctx.OrigIssue.updateCrossReferenceList(xreflist, &crossReference{
|
||||
Issue: refIssue,
|
||||
Action: refAction,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return xreflist, nil
|
||||
}
|
||||
|
||||
func (issue *Issue) updateCrossReferenceList(list []*crossReference, xref *crossReference) []*crossReference {
|
||||
if xref.Issue.ID == issue.ID {
|
||||
return list
|
||||
}
|
||||
for i, r := range list {
|
||||
if r.Issue.ID == xref.Issue.ID {
|
||||
if xref.Action != references.XRefActionNone {
|
||||
list[i].Action = xref.Action
|
||||
}
|
||||
return list
|
||||
}
|
||||
}
|
||||
return append(list, xref)
|
||||
}
|
||||
|
||||
// verifyReferencedIssue will check if the referenced issue exists, and whether the doer has permission to do what
|
||||
func (issue *Issue) verifyReferencedIssue(stdCtx context.Context, ctx *crossReferencesContext, repo *repo_model.Repository,
|
||||
ref references.IssueReference,
|
||||
) (*Issue, references.XRefAction, error) {
|
||||
refIssue := &Issue{RepoID: repo.ID, Index: ref.Index}
|
||||
refAction := ref.Action
|
||||
e := db.GetEngine(stdCtx)
|
||||
|
||||
if has, _ := e.Get(refIssue); !has {
|
||||
return nil, references.XRefActionNone, nil
|
||||
}
|
||||
if err := refIssue.LoadRepo(stdCtx); err != nil {
|
||||
return nil, references.XRefActionNone, err
|
||||
}
|
||||
|
||||
// Close/reopen actions can only be set from pull requests to issues
|
||||
if refIssue.IsPull || !issue.IsPull {
|
||||
refAction = references.XRefActionNone
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, references.XRefActionNone, err
|
||||
}
|
||||
if !perm.CanReadIssuesOrPulls(refIssue.IsPull) {
|
||||
return nil, references.XRefActionNone, nil
|
||||
}
|
||||
// Accept close/reopening actions only if the poster is able to close the
|
||||
// referenced issue manually at this moment. The only exception is
|
||||
// the poster of a new PR referencing an issue on the same repo: then the merger
|
||||
// should be responsible for checking whether the reference should resolve.
|
||||
if ref.Action != references.XRefActionNone &&
|
||||
ctx.Doer.ID != refIssue.PosterID &&
|
||||
!perm.CanWriteIssuesOrPulls(refIssue.IsPull) &&
|
||||
(refIssue.RepoID != ctx.OrigIssue.RepoID || ctx.OrigComment != nil) {
|
||||
refAction = references.XRefActionNone
|
||||
}
|
||||
}
|
||||
|
||||
return refIssue, refAction, nil
|
||||
}
|
||||
|
||||
// AddCrossReferences add cross references
|
||||
func (comment *Comment) AddCrossReferences(stdCtx context.Context, doer *user_model.User, removeOld bool) error {
|
||||
if comment.Type != CommentTypeCode && comment.Type != CommentTypeComment {
|
||||
return nil
|
||||
}
|
||||
if err := comment.LoadIssueCtx(stdCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := &crossReferencesContext{
|
||||
Type: CommentTypeCommentRef,
|
||||
Doer: doer,
|
||||
OrigIssue: comment.Issue,
|
||||
OrigComment: comment,
|
||||
RemoveOld: removeOld,
|
||||
}
|
||||
return comment.Issue.createCrossReferences(stdCtx, ctx, "", comment.Content)
|
||||
}
|
||||
|
||||
func (comment *Comment) neuterCrossReferences(ctx context.Context) error {
|
||||
return neuterCrossReferences(ctx, comment.IssueID, comment.ID)
|
||||
}
|
||||
|
||||
// LoadRefComment loads comment that created this reference from database
|
||||
func (comment *Comment) LoadRefComment() (err error) {
|
||||
if comment.RefComment != nil {
|
||||
return nil
|
||||
}
|
||||
comment.RefComment, err = GetCommentByID(db.DefaultContext, comment.RefCommentID)
|
||||
return
|
||||
}
|
||||
|
||||
// LoadRefIssue loads comment that created this reference from database
|
||||
func (comment *Comment) LoadRefIssue() (err error) {
|
||||
if comment.RefIssue != nil {
|
||||
return nil
|
||||
}
|
||||
comment.RefIssue, err = GetIssueByID(db.DefaultContext, comment.RefIssueID)
|
||||
if err == nil {
|
||||
err = comment.RefIssue.LoadRepo(db.DefaultContext)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CommentTypeIsRef returns true if CommentType is a reference from another issue
|
||||
func CommentTypeIsRef(t CommentType) bool {
|
||||
return t == CommentTypeCommentRef || t == CommentTypePullRef || t == CommentTypeIssueRef
|
||||
}
|
||||
|
||||
// RefCommentHTMLURL returns the HTML URL for the comment that created this reference
|
||||
func (comment *Comment) RefCommentHTMLURL() string {
|
||||
// Edge case for when the reference is inside the title or the description of the referring issue
|
||||
if comment.RefCommentID == 0 {
|
||||
return comment.RefIssueHTMLURL()
|
||||
}
|
||||
if err := comment.LoadRefComment(); err != nil { // Silently dropping errors :unamused:
|
||||
log.Error("LoadRefComment(%d): %v", comment.RefCommentID, err)
|
||||
return ""
|
||||
}
|
||||
return comment.RefComment.HTMLURL()
|
||||
}
|
||||
|
||||
// RefIssueHTMLURL returns the HTML URL of the issue where this reference was created
|
||||
func (comment *Comment) RefIssueHTMLURL() string {
|
||||
if err := comment.LoadRefIssue(); err != nil { // Silently dropping errors :unamused:
|
||||
log.Error("LoadRefIssue(%d): %v", comment.RefCommentID, err)
|
||||
return ""
|
||||
}
|
||||
return comment.RefIssue.HTMLURL()
|
||||
}
|
||||
|
||||
// RefIssueTitle returns the title of the issue where this reference was created
|
||||
func (comment *Comment) RefIssueTitle() string {
|
||||
if err := comment.LoadRefIssue(); err != nil { // Silently dropping errors :unamused:
|
||||
log.Error("LoadRefIssue(%d): %v", comment.RefCommentID, err)
|
||||
return ""
|
||||
}
|
||||
return comment.RefIssue.Title
|
||||
}
|
||||
|
||||
// RefIssueIdent returns the user friendly identity (e.g. "#1234") of the issue where this reference was created
|
||||
func (comment *Comment) RefIssueIdent() string {
|
||||
if err := comment.LoadRefIssue(); err != nil { // Silently dropping errors :unamused:
|
||||
log.Error("LoadRefIssue(%d): %v", comment.RefCommentID, err)
|
||||
return ""
|
||||
}
|
||||
// FIXME: check this name for cross-repository references (#7901 if it gets merged)
|
||||
return fmt.Sprintf("#%d", comment.RefIssue.Index)
|
||||
}
|
||||
|
||||
// __________ .__ .__ __________ __
|
||||
// \______ \__ __| | | |\______ \ ____ ________ __ ____ _______/ |_
|
||||
// | ___/ | \ | | | | _// __ \/ ____/ | \_/ __ \ / ___/\ __\
|
||||
// | | | | / |_| |_| | \ ___< <_| | | /\ ___/ \___ \ | |
|
||||
// |____| |____/|____/____/____|_ /\___ >__ |____/ \___ >____ > |__|
|
||||
// \/ \/ |__| \/ \/
|
||||
|
||||
// ResolveCrossReferences will return the list of references to close/reopen by this PR
|
||||
func (pr *PullRequest) ResolveCrossReferences(ctx context.Context) ([]*Comment, error) {
|
||||
unfiltered := make([]*Comment, 0, 5)
|
||||
if err := db.GetEngine(ctx).
|
||||
Where("ref_repo_id = ? AND ref_issue_id = ?", pr.Issue.RepoID, pr.Issue.ID).
|
||||
In("ref_action", []references.XRefAction{references.XRefActionCloses, references.XRefActionReopens}).
|
||||
OrderBy("id").
|
||||
Find(&unfiltered); err != nil {
|
||||
return nil, fmt.Errorf("get reference: %v", err)
|
||||
}
|
||||
|
||||
refs := make([]*Comment, 0, len(unfiltered))
|
||||
for _, ref := range unfiltered {
|
||||
found := false
|
||||
for i, r := range refs {
|
||||
if r.IssueID == ref.IssueID {
|
||||
// Keep only the latest
|
||||
refs[i] = ref
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
refs = append(refs, ref)
|
||||
}
|
||||
}
|
||||
|
||||
return refs, nil
|
||||
}
|
184
models/issues/issue_xref_test.go
Normal file
184
models/issues/issue_xref_test.go
Normal file
|
@ -0,0 +1,184 @@
|
|||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/references"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestXRef_AddCrossReferences(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// Issue #1 to test against
|
||||
itarget := testCreateIssue(t, 1, 2, "title1", "content1", false)
|
||||
|
||||
// PR to close issue #1
|
||||
content := fmt.Sprintf("content2, closes #%d", itarget.Index)
|
||||
pr := testCreateIssue(t, 1, 2, "title2", content, true)
|
||||
ref := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: pr.ID, RefCommentID: 0}).(*issues_model.Comment)
|
||||
assert.Equal(t, issues_model.CommentTypePullRef, ref.Type)
|
||||
assert.Equal(t, pr.RepoID, ref.RefRepoID)
|
||||
assert.True(t, ref.RefIsPull)
|
||||
assert.Equal(t, references.XRefActionCloses, ref.RefAction)
|
||||
|
||||
// Comment on PR to reopen issue #1
|
||||
content = fmt.Sprintf("content2, reopens #%d", itarget.Index)
|
||||
c := testCreateComment(t, 1, 2, pr.ID, content)
|
||||
ref = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: pr.ID, RefCommentID: c.ID}).(*issues_model.Comment)
|
||||
assert.Equal(t, issues_model.CommentTypeCommentRef, ref.Type)
|
||||
assert.Equal(t, pr.RepoID, ref.RefRepoID)
|
||||
assert.True(t, ref.RefIsPull)
|
||||
assert.Equal(t, references.XRefActionReopens, ref.RefAction)
|
||||
|
||||
// Issue mentioning issue #1
|
||||
content = fmt.Sprintf("content3, mentions #%d", itarget.Index)
|
||||
i := testCreateIssue(t, 1, 2, "title3", content, false)
|
||||
ref = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*issues_model.Comment)
|
||||
assert.Equal(t, issues_model.CommentTypeIssueRef, ref.Type)
|
||||
assert.Equal(t, pr.RepoID, ref.RefRepoID)
|
||||
assert.False(t, ref.RefIsPull)
|
||||
assert.Equal(t, references.XRefActionNone, ref.RefAction)
|
||||
|
||||
// Issue #4 to test against
|
||||
itarget = testCreateIssue(t, 3, 3, "title4", "content4", false)
|
||||
|
||||
// Cross-reference to issue #4 by admin
|
||||
content = fmt.Sprintf("content5, mentions user3/repo3#%d", itarget.Index)
|
||||
i = testCreateIssue(t, 2, 1, "title5", content, false)
|
||||
ref = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*issues_model.Comment)
|
||||
assert.Equal(t, issues_model.CommentTypeIssueRef, ref.Type)
|
||||
assert.Equal(t, i.RepoID, ref.RefRepoID)
|
||||
assert.False(t, ref.RefIsPull)
|
||||
assert.Equal(t, references.XRefActionNone, ref.RefAction)
|
||||
|
||||
// Cross-reference to issue #4 with no permission
|
||||
content = fmt.Sprintf("content6, mentions user3/repo3#%d", itarget.Index)
|
||||
i = testCreateIssue(t, 4, 5, "title6", content, false)
|
||||
unittest.AssertNotExistsBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0})
|
||||
}
|
||||
|
||||
func TestXRef_NeuterCrossReferences(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// Issue #1 to test against
|
||||
itarget := testCreateIssue(t, 1, 2, "title1", "content1", false)
|
||||
|
||||
// Issue mentioning issue #1
|
||||
title := fmt.Sprintf("title2, mentions #%d", itarget.Index)
|
||||
i := testCreateIssue(t, 1, 2, title, "content2", false)
|
||||
ref := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*issues_model.Comment)
|
||||
assert.Equal(t, issues_model.CommentTypeIssueRef, ref.Type)
|
||||
assert.Equal(t, references.XRefActionNone, ref.RefAction)
|
||||
|
||||
d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
|
||||
i.Title = "title2, no mentions"
|
||||
assert.NoError(t, issues_model.ChangeIssueTitle(i, d, title))
|
||||
|
||||
ref = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: itarget.ID, RefIssueID: i.ID, RefCommentID: 0}).(*issues_model.Comment)
|
||||
assert.Equal(t, issues_model.CommentTypeIssueRef, ref.Type)
|
||||
assert.Equal(t, references.XRefActionNeutered, ref.RefAction)
|
||||
}
|
||||
|
||||
func TestXRef_ResolveCrossReferences(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
|
||||
|
||||
i1 := testCreateIssue(t, 1, 2, "title1", "content1", false)
|
||||
i2 := testCreateIssue(t, 1, 2, "title2", "content2", false)
|
||||
i3 := testCreateIssue(t, 1, 2, "title3", "content3", false)
|
||||
_, err := issues_model.ChangeIssueStatus(db.DefaultContext, i3, d, true)
|
||||
assert.NoError(t, err)
|
||||
|
||||
pr := testCreatePR(t, 1, 2, "titlepr", fmt.Sprintf("closes #%d", i1.Index))
|
||||
rp := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i1.ID, RefIssueID: pr.Issue.ID, RefCommentID: 0}).(*issues_model.Comment)
|
||||
|
||||
c1 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("closes #%d", i2.Index))
|
||||
r1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i2.ID, RefIssueID: pr.Issue.ID, RefCommentID: c1.ID}).(*issues_model.Comment)
|
||||
|
||||
// Must be ignored
|
||||
c2 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("mentions #%d", i2.Index))
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i2.ID, RefIssueID: pr.Issue.ID, RefCommentID: c2.ID})
|
||||
|
||||
// Must be superseded by c4/r4
|
||||
c3 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("reopens #%d", i3.Index))
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i3.ID, RefIssueID: pr.Issue.ID, RefCommentID: c3.ID})
|
||||
|
||||
c4 := testCreateComment(t, 1, 2, pr.Issue.ID, fmt.Sprintf("closes #%d", i3.Index))
|
||||
r4 := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{IssueID: i3.ID, RefIssueID: pr.Issue.ID, RefCommentID: c4.ID}).(*issues_model.Comment)
|
||||
|
||||
refs, err := pr.ResolveCrossReferences(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, refs, 3)
|
||||
assert.Equal(t, rp.ID, refs[0].ID, "bad ref rp: %+v", refs[0])
|
||||
assert.Equal(t, r1.ID, refs[1].ID, "bad ref r1: %+v", refs[1])
|
||||
assert.Equal(t, r4.ID, refs[2].ID, "bad ref r4: %+v", refs[2])
|
||||
}
|
||||
|
||||
func testCreateIssue(t *testing.T, repo, doer int64, title, content string, ispull bool) *issues_model.Issue {
|
||||
r := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo}).(*repo_model.Repository)
|
||||
d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doer}).(*user_model.User)
|
||||
|
||||
idx, err := db.GetNextResourceIndex("issue_index", r.ID)
|
||||
assert.NoError(t, err)
|
||||
i := &issues_model.Issue{
|
||||
RepoID: r.ID,
|
||||
PosterID: d.ID,
|
||||
Poster: d,
|
||||
Title: title,
|
||||
Content: content,
|
||||
IsPull: ispull,
|
||||
Index: idx,
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext()
|
||||
assert.NoError(t, err)
|
||||
defer committer.Close()
|
||||
err = issues_model.NewIssueWithIndex(ctx, d, issues_model.NewIssueOptions{
|
||||
Repo: r,
|
||||
Issue: i,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
i, err = issues_model.GetIssueByID(ctx, i.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, i.AddCrossReferences(ctx, d, false))
|
||||
assert.NoError(t, committer.Commit())
|
||||
return i
|
||||
}
|
||||
|
||||
func testCreatePR(t *testing.T, repo, doer int64, title, content string) *issues_model.PullRequest {
|
||||
r := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo}).(*repo_model.Repository)
|
||||
d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doer}).(*user_model.User)
|
||||
i := &issues_model.Issue{RepoID: r.ID, PosterID: d.ID, Poster: d, Title: title, Content: content, IsPull: true}
|
||||
pr := &issues_model.PullRequest{HeadRepoID: repo, BaseRepoID: repo, HeadBranch: "head", BaseBranch: "base", Status: issues_model.PullRequestStatusMergeable}
|
||||
assert.NoError(t, issues_model.NewPullRequest(db.DefaultContext, r, i, nil, nil, pr))
|
||||
pr.Issue = i
|
||||
return pr
|
||||
}
|
||||
|
||||
func testCreateComment(t *testing.T, repo, doer, issue int64, content string) *issues_model.Comment {
|
||||
d := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doer}).(*user_model.User)
|
||||
i := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issue}).(*issues_model.Issue)
|
||||
c := &issues_model.Comment{Type: issues_model.CommentTypeComment, PosterID: doer, Poster: d, IssueID: issue, Issue: i, Content: content}
|
||||
|
||||
ctx, committer, err := db.TxContext()
|
||||
assert.NoError(t, err)
|
||||
defer committer.Close()
|
||||
err = db.Insert(ctx, c)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, c.AddCrossReferences(ctx, d, false))
|
||||
assert.NoError(t, committer.Commit())
|
||||
return c
|
||||
}
|
836
models/issues/label.go
Normal file
836
models/issues/label.go
Normal file
|
@ -0,0 +1,836 @@
|
|||
// Copyright 2016 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2020 The Gitea Authors.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"math"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ErrRepoLabelNotExist represents a "RepoLabelNotExist" kind of error.
|
||||
type ErrRepoLabelNotExist struct {
|
||||
LabelID int64
|
||||
RepoID int64
|
||||
}
|
||||
|
||||
// IsErrRepoLabelNotExist checks if an error is a RepoErrLabelNotExist.
|
||||
func IsErrRepoLabelNotExist(err error) bool {
|
||||
_, ok := err.(ErrRepoLabelNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrRepoLabelNotExist) Error() string {
|
||||
return fmt.Sprintf("label does not exist [label_id: %d, repo_id: %d]", err.LabelID, err.RepoID)
|
||||
}
|
||||
|
||||
// ErrOrgLabelNotExist represents a "OrgLabelNotExist" kind of error.
|
||||
type ErrOrgLabelNotExist struct {
|
||||
LabelID int64
|
||||
OrgID int64
|
||||
}
|
||||
|
||||
// IsErrOrgLabelNotExist checks if an error is a OrgErrLabelNotExist.
|
||||
func IsErrOrgLabelNotExist(err error) bool {
|
||||
_, ok := err.(ErrOrgLabelNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrOrgLabelNotExist) Error() string {
|
||||
return fmt.Sprintf("label does not exist [label_id: %d, org_id: %d]", err.LabelID, err.OrgID)
|
||||
}
|
||||
|
||||
// ErrLabelNotExist represents a "LabelNotExist" kind of error.
|
||||
type ErrLabelNotExist struct {
|
||||
LabelID int64
|
||||
}
|
||||
|
||||
// IsErrLabelNotExist checks if an error is a ErrLabelNotExist.
|
||||
func IsErrLabelNotExist(err error) bool {
|
||||
_, ok := err.(ErrLabelNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrLabelNotExist) Error() string {
|
||||
return fmt.Sprintf("label does not exist [label_id: %d]", err.LabelID)
|
||||
}
|
||||
|
||||
// LabelColorPattern is a regexp witch can validate LabelColor
|
||||
var LabelColorPattern = regexp.MustCompile("^#?(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{3})$")
|
||||
|
||||
// Label represents a label of repository for issues.
|
||||
type Label struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"INDEX"`
|
||||
OrgID int64 `xorm:"INDEX"`
|
||||
Name string
|
||||
Description string
|
||||
Color string `xorm:"VARCHAR(7)"`
|
||||
NumIssues int
|
||||
NumClosedIssues int
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||
|
||||
NumOpenIssues int `xorm:"-"`
|
||||
NumOpenRepoIssues int64 `xorm:"-"`
|
||||
IsChecked bool `xorm:"-"`
|
||||
QueryString string `xorm:"-"`
|
||||
IsSelected bool `xorm:"-"`
|
||||
IsExcluded bool `xorm:"-"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(Label))
|
||||
db.RegisterModel(new(IssueLabel))
|
||||
}
|
||||
|
||||
// CalOpenIssues sets the number of open issues of a label based on the already stored number of closed issues.
|
||||
func (label *Label) CalOpenIssues() {
|
||||
label.NumOpenIssues = label.NumIssues - label.NumClosedIssues
|
||||
}
|
||||
|
||||
// CalOpenOrgIssues calculates the open issues of a label for a specific repo
|
||||
func (label *Label) CalOpenOrgIssues(repoID, labelID int64) {
|
||||
counts, _ := CountIssuesByRepo(&IssuesOptions{
|
||||
RepoID: repoID,
|
||||
LabelIDs: []int64{labelID},
|
||||
})
|
||||
|
||||
for _, count := range counts {
|
||||
label.NumOpenRepoIssues += count
|
||||
}
|
||||
}
|
||||
|
||||
// LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked
|
||||
func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64) {
|
||||
var labelQuerySlice []string
|
||||
labelSelected := false
|
||||
labelID := strconv.FormatInt(label.ID, 10)
|
||||
for _, s := range currentSelectedLabels {
|
||||
if s == label.ID {
|
||||
labelSelected = true
|
||||
} else if -s == label.ID {
|
||||
labelSelected = true
|
||||
label.IsExcluded = true
|
||||
} else if s != 0 {
|
||||
labelQuerySlice = append(labelQuerySlice, strconv.FormatInt(s, 10))
|
||||
}
|
||||
}
|
||||
if !labelSelected {
|
||||
labelQuerySlice = append(labelQuerySlice, labelID)
|
||||
}
|
||||
label.IsSelected = labelSelected
|
||||
label.QueryString = strings.Join(labelQuerySlice, ",")
|
||||
}
|
||||
|
||||
// BelongsToOrg returns true if label is an organization label
|
||||
func (label *Label) BelongsToOrg() bool {
|
||||
return label.OrgID > 0
|
||||
}
|
||||
|
||||
// BelongsToRepo returns true if label is a repository label
|
||||
func (label *Label) BelongsToRepo() bool {
|
||||
return label.RepoID > 0
|
||||
}
|
||||
|
||||
// SrgbToLinear converts a component of an sRGB color to its linear intensity
|
||||
// See: https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation_(sRGB_to_CIE_XYZ)
|
||||
func SrgbToLinear(color uint8) float64 {
|
||||
flt := float64(color) / 255
|
||||
if flt <= 0.04045 {
|
||||
return flt / 12.92
|
||||
}
|
||||
return math.Pow((flt+0.055)/1.055, 2.4)
|
||||
}
|
||||
|
||||
// Luminance returns the luminance of an sRGB color
|
||||
func Luminance(color uint32) float64 {
|
||||
r := SrgbToLinear(uint8(0xFF & (color >> 16)))
|
||||
g := SrgbToLinear(uint8(0xFF & (color >> 8)))
|
||||
b := SrgbToLinear(uint8(0xFF & color))
|
||||
|
||||
// luminance ratios for sRGB
|
||||
return 0.2126*r + 0.7152*g + 0.0722*b
|
||||
}
|
||||
|
||||
// LuminanceThreshold is the luminance at which white and black appear to have the same contrast
|
||||
// i.e. x such that 1.05 / (x + 0.05) = (x + 0.05) / 0.05
|
||||
// i.e. math.Sqrt(1.05*0.05) - 0.05
|
||||
const LuminanceThreshold float64 = 0.179
|
||||
|
||||
// ForegroundColor calculates the text color for labels based
|
||||
// on their background color.
|
||||
func (label *Label) ForegroundColor() template.CSS {
|
||||
if strings.HasPrefix(label.Color, "#") {
|
||||
if color, err := strconv.ParseUint(label.Color[1:], 16, 64); err == nil {
|
||||
// NOTE: see web_src/js/components/ContextPopup.vue for similar implementation
|
||||
luminance := Luminance(uint32(color))
|
||||
|
||||
// prefer white or black based upon contrast
|
||||
if luminance < LuminanceThreshold {
|
||||
return template.CSS("#fff")
|
||||
}
|
||||
return template.CSS("#000")
|
||||
}
|
||||
}
|
||||
|
||||
// default to black
|
||||
return template.CSS("#000")
|
||||
}
|
||||
|
||||
// NewLabel creates a new label
|
||||
func NewLabel(ctx context.Context, label *Label) error {
|
||||
if !LabelColorPattern.MatchString(label.Color) {
|
||||
return fmt.Errorf("bad color code: %s", label.Color)
|
||||
}
|
||||
|
||||
// normalize case
|
||||
label.Color = strings.ToLower(label.Color)
|
||||
|
||||
// add leading hash
|
||||
if label.Color[0] != '#' {
|
||||
label.Color = "#" + label.Color
|
||||
}
|
||||
|
||||
// convert 3-character shorthand into 6-character version
|
||||
if len(label.Color) == 4 {
|
||||
r := label.Color[1]
|
||||
g := label.Color[2]
|
||||
b := label.Color[3]
|
||||
label.Color = fmt.Sprintf("#%c%c%c%c%c%c", r, r, g, g, b, b)
|
||||
}
|
||||
|
||||
return db.Insert(ctx, label)
|
||||
}
|
||||
|
||||
// NewLabels creates new labels
|
||||
func NewLabels(labels ...*Label) error {
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
for _, label := range labels {
|
||||
if !LabelColorPattern.MatchString(label.Color) {
|
||||
return fmt.Errorf("bad color code: %s", label.Color)
|
||||
}
|
||||
if err := db.Insert(ctx, label); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// UpdateLabel updates label information.
|
||||
func UpdateLabel(l *Label) error {
|
||||
if !LabelColorPattern.MatchString(l.Color) {
|
||||
return fmt.Errorf("bad color code: %s", l.Color)
|
||||
}
|
||||
return updateLabelCols(db.DefaultContext, l, "name", "description", "color")
|
||||
}
|
||||
|
||||
// DeleteLabel delete a label
|
||||
func DeleteLabel(id, labelID int64) error {
|
||||
label, err := GetLabelByID(db.DefaultContext, labelID)
|
||||
if err != nil {
|
||||
if IsErrLabelNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
if label.BelongsToOrg() && label.OrgID != id {
|
||||
return nil
|
||||
}
|
||||
if label.BelongsToRepo() && label.RepoID != id {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err = sess.ID(labelID).Delete(new(Label)); err != nil {
|
||||
return err
|
||||
} else if _, err = sess.
|
||||
Where("label_id = ?", labelID).
|
||||
Delete(new(IssueLabel)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete comments about now deleted label_id
|
||||
if _, err = sess.Where("label_id = ?", labelID).Cols("label_id").Delete(&Comment{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// GetLabelByID returns a label by given ID.
|
||||
func GetLabelByID(ctx context.Context, labelID int64) (*Label, error) {
|
||||
if labelID <= 0 {
|
||||
return nil, ErrLabelNotExist{labelID}
|
||||
}
|
||||
|
||||
l := &Label{}
|
||||
has, err := db.GetEngine(ctx).ID(labelID).Get(l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrLabelNotExist{l.ID}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// GetLabelsByIDs returns a list of labels by IDs
|
||||
func GetLabelsByIDs(labelIDs []int64) ([]*Label, error) {
|
||||
labels := make([]*Label, 0, len(labelIDs))
|
||||
return labels, db.GetEngine(db.DefaultContext).Table("label").
|
||||
In("id", labelIDs).
|
||||
Asc("name").
|
||||
Cols("id", "repo_id", "org_id").
|
||||
Find(&labels)
|
||||
}
|
||||
|
||||
// __________ .__ __
|
||||
// \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__.
|
||||
// | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | |
|
||||
// | | \ ___/| |_> > <_> )___ \| || | ( <_> ) | \/\___ |
|
||||
// |____|_ /\___ > __/ \____/____ >__||__| \____/|__| / ____|
|
||||
// \/ \/|__| \/ \/
|
||||
|
||||
// GetLabelInRepoByName returns a label by name in given repository.
|
||||
func GetLabelInRepoByName(ctx context.Context, repoID int64, labelName string) (*Label, error) {
|
||||
if len(labelName) == 0 || repoID <= 0 {
|
||||
return nil, ErrRepoLabelNotExist{0, repoID}
|
||||
}
|
||||
|
||||
l := &Label{
|
||||
Name: labelName,
|
||||
RepoID: repoID,
|
||||
}
|
||||
has, err := db.GetByBean(ctx, l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrRepoLabelNotExist{0, l.RepoID}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// GetLabelInRepoByID returns a label by ID in given repository.
|
||||
func GetLabelInRepoByID(ctx context.Context, repoID, labelID int64) (*Label, error) {
|
||||
if labelID <= 0 || repoID <= 0 {
|
||||
return nil, ErrRepoLabelNotExist{labelID, repoID}
|
||||
}
|
||||
|
||||
l := &Label{
|
||||
ID: labelID,
|
||||
RepoID: repoID,
|
||||
}
|
||||
has, err := db.GetByBean(ctx, l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrRepoLabelNotExist{l.ID, l.RepoID}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// GetLabelIDsInRepoByNames returns a list of labelIDs by names in a given
|
||||
// repository.
|
||||
// it silently ignores label names that do not belong to the repository.
|
||||
func GetLabelIDsInRepoByNames(repoID int64, labelNames []string) ([]int64, error) {
|
||||
labelIDs := make([]int64, 0, len(labelNames))
|
||||
return labelIDs, db.GetEngine(db.DefaultContext).Table("label").
|
||||
Where("repo_id = ?", repoID).
|
||||
In("name", labelNames).
|
||||
Asc("name").
|
||||
Cols("id").
|
||||
Find(&labelIDs)
|
||||
}
|
||||
|
||||
// BuildLabelNamesIssueIDsCondition returns a builder where get issue ids match label names
|
||||
func BuildLabelNamesIssueIDsCondition(labelNames []string) *builder.Builder {
|
||||
return builder.Select("issue_label.issue_id").
|
||||
From("issue_label").
|
||||
InnerJoin("label", "label.id = issue_label.label_id").
|
||||
Where(
|
||||
builder.In("label.name", labelNames),
|
||||
).
|
||||
GroupBy("issue_label.issue_id")
|
||||
}
|
||||
|
||||
// GetLabelsInRepoByIDs returns a list of labels by IDs in given repository,
|
||||
// it silently ignores label IDs that do not belong to the repository.
|
||||
func GetLabelsInRepoByIDs(repoID int64, labelIDs []int64) ([]*Label, error) {
|
||||
labels := make([]*Label, 0, len(labelIDs))
|
||||
return labels, db.GetEngine(db.DefaultContext).
|
||||
Where("repo_id = ?", repoID).
|
||||
In("id", labelIDs).
|
||||
Asc("name").
|
||||
Find(&labels)
|
||||
}
|
||||
|
||||
// GetLabelsByRepoID returns all labels that belong to given repository by ID.
|
||||
func GetLabelsByRepoID(ctx context.Context, repoID int64, sortType string, listOptions db.ListOptions) ([]*Label, error) {
|
||||
if repoID <= 0 {
|
||||
return nil, ErrRepoLabelNotExist{0, repoID}
|
||||
}
|
||||
labels := make([]*Label, 0, 10)
|
||||
sess := db.GetEngine(ctx).Where("repo_id = ?", repoID)
|
||||
|
||||
switch sortType {
|
||||
case "reversealphabetically":
|
||||
sess.Desc("name")
|
||||
case "leastissues":
|
||||
sess.Asc("num_issues")
|
||||
case "mostissues":
|
||||
sess.Desc("num_issues")
|
||||
default:
|
||||
sess.Asc("name")
|
||||
}
|
||||
|
||||
if listOptions.Page != 0 {
|
||||
sess = db.SetSessionPagination(sess, &listOptions)
|
||||
}
|
||||
|
||||
return labels, sess.Find(&labels)
|
||||
}
|
||||
|
||||
// CountLabelsByRepoID count number of all labels that belong to given repository by ID.
|
||||
func CountLabelsByRepoID(repoID int64) (int64, error) {
|
||||
return db.GetEngine(db.DefaultContext).Where("repo_id = ?", repoID).Count(&Label{})
|
||||
}
|
||||
|
||||
// ________
|
||||
// \_____ \_______ ____
|
||||
// / | \_ __ \/ ___\
|
||||
// / | \ | \/ /_/ >
|
||||
// \_______ /__| \___ /
|
||||
// \/ /_____/
|
||||
|
||||
// GetLabelInOrgByName returns a label by name in given organization.
|
||||
func GetLabelInOrgByName(ctx context.Context, orgID int64, labelName string) (*Label, error) {
|
||||
if len(labelName) == 0 || orgID <= 0 {
|
||||
return nil, ErrOrgLabelNotExist{0, orgID}
|
||||
}
|
||||
|
||||
l := &Label{
|
||||
Name: labelName,
|
||||
OrgID: orgID,
|
||||
}
|
||||
has, err := db.GetByBean(ctx, l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrOrgLabelNotExist{0, l.OrgID}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// GetLabelInOrgByID returns a label by ID in given organization.
|
||||
func GetLabelInOrgByID(ctx context.Context, orgID, labelID int64) (*Label, error) {
|
||||
if labelID <= 0 || orgID <= 0 {
|
||||
return nil, ErrOrgLabelNotExist{labelID, orgID}
|
||||
}
|
||||
|
||||
l := &Label{
|
||||
ID: labelID,
|
||||
OrgID: orgID,
|
||||
}
|
||||
has, err := db.GetByBean(ctx, l)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrOrgLabelNotExist{l.ID, l.OrgID}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
||||
// GetLabelIDsInOrgByNames returns a list of labelIDs by names in a given
|
||||
// organization.
|
||||
func GetLabelIDsInOrgByNames(orgID int64, labelNames []string) ([]int64, error) {
|
||||
if orgID <= 0 {
|
||||
return nil, ErrOrgLabelNotExist{0, orgID}
|
||||
}
|
||||
labelIDs := make([]int64, 0, len(labelNames))
|
||||
|
||||
return labelIDs, db.GetEngine(db.DefaultContext).Table("label").
|
||||
Where("org_id = ?", orgID).
|
||||
In("name", labelNames).
|
||||
Asc("name").
|
||||
Cols("id").
|
||||
Find(&labelIDs)
|
||||
}
|
||||
|
||||
// GetLabelsInOrgByIDs returns a list of labels by IDs in given organization,
|
||||
// it silently ignores label IDs that do not belong to the organization.
|
||||
func GetLabelsInOrgByIDs(orgID int64, labelIDs []int64) ([]*Label, error) {
|
||||
labels := make([]*Label, 0, len(labelIDs))
|
||||
return labels, db.GetEngine(db.DefaultContext).
|
||||
Where("org_id = ?", orgID).
|
||||
In("id", labelIDs).
|
||||
Asc("name").
|
||||
Find(&labels)
|
||||
}
|
||||
|
||||
// GetLabelsByOrgID returns all labels that belong to given organization by ID.
|
||||
func GetLabelsByOrgID(ctx context.Context, orgID int64, sortType string, listOptions db.ListOptions) ([]*Label, error) {
|
||||
if orgID <= 0 {
|
||||
return nil, ErrOrgLabelNotExist{0, orgID}
|
||||
}
|
||||
labels := make([]*Label, 0, 10)
|
||||
sess := db.GetEngine(ctx).Where("org_id = ?", orgID)
|
||||
|
||||
switch sortType {
|
||||
case "reversealphabetically":
|
||||
sess.Desc("name")
|
||||
case "leastissues":
|
||||
sess.Asc("num_issues")
|
||||
case "mostissues":
|
||||
sess.Desc("num_issues")
|
||||
default:
|
||||
sess.Asc("name")
|
||||
}
|
||||
|
||||
if listOptions.Page != 0 {
|
||||
sess = db.SetSessionPagination(sess, &listOptions)
|
||||
}
|
||||
|
||||
return labels, sess.Find(&labels)
|
||||
}
|
||||
|
||||
// CountLabelsByOrgID count all labels that belong to given organization by ID.
|
||||
func CountLabelsByOrgID(orgID int64) (int64, error) {
|
||||
return db.GetEngine(db.DefaultContext).Where("org_id = ?", orgID).Count(&Label{})
|
||||
}
|
||||
|
||||
// .___
|
||||
// | | ______ ________ __ ____
|
||||
// | |/ ___// ___/ | \_/ __ \
|
||||
// | |\___ \ \___ \| | /\ ___/
|
||||
// |___/____ >____ >____/ \___ |
|
||||
// \/ \/ \/
|
||||
|
||||
// GetLabelsByIssueID returns all labels that belong to given issue by ID.
|
||||
func GetLabelsByIssueID(ctx context.Context, issueID int64) ([]*Label, error) {
|
||||
var labels []*Label
|
||||
return labels, db.GetEngine(ctx).Where("issue_label.issue_id = ?", issueID).
|
||||
Join("LEFT", "issue_label", "issue_label.label_id = label.id").
|
||||
Asc("label.name").
|
||||
Find(&labels)
|
||||
}
|
||||
|
||||
func updateLabelCols(ctx context.Context, l *Label, cols ...string) error {
|
||||
_, err := db.GetEngine(ctx).ID(l.ID).
|
||||
SetExpr("num_issues",
|
||||
builder.Select("count(*)").From("issue_label").
|
||||
Where(builder.Eq{"label_id": l.ID}),
|
||||
).
|
||||
SetExpr("num_closed_issues",
|
||||
builder.Select("count(*)").From("issue_label").
|
||||
InnerJoin("issue", "issue_label.issue_id = issue.id").
|
||||
Where(builder.Eq{
|
||||
"issue_label.label_id": l.ID,
|
||||
"issue.is_closed": true,
|
||||
}),
|
||||
).
|
||||
Cols(cols...).Update(l)
|
||||
return err
|
||||
}
|
||||
|
||||
// .___ .____ ___. .__
|
||||
// | | ______ ________ __ ____ | | _____ \_ |__ ____ | |
|
||||
// | |/ ___// ___/ | \_/ __ \| | \__ \ | __ \_/ __ \| |
|
||||
// | |\___ \ \___ \| | /\ ___/| |___ / __ \| \_\ \ ___/| |__
|
||||
// |___/____ >____ >____/ \___ >_______ (____ /___ /\___ >____/
|
||||
// \/ \/ \/ \/ \/ \/ \/
|
||||
|
||||
// IssueLabel represents an issue-label relation.
|
||||
type IssueLabel struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
IssueID int64 `xorm:"UNIQUE(s)"`
|
||||
LabelID int64 `xorm:"UNIQUE(s)"`
|
||||
}
|
||||
|
||||
// HasIssueLabel returns true if issue has been labeled.
|
||||
func HasIssueLabel(ctx context.Context, issueID, labelID int64) bool {
|
||||
has, _ := db.GetEngine(ctx).Where("issue_id = ? AND label_id = ?", issueID, labelID).Get(new(IssueLabel))
|
||||
return has
|
||||
}
|
||||
|
||||
// newIssueLabel this function creates a new label it does not check if the label is valid for the issue
|
||||
// YOU MUST CHECK THIS BEFORE THIS FUNCTION
|
||||
func newIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) {
|
||||
if err = db.Insert(ctx, &IssueLabel{
|
||||
IssueID: issue.ID,
|
||||
LabelID: label.ID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = issue.LoadRepo(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
opts := &CreateCommentOptions{
|
||||
Type: CommentTypeLabel,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
Label: label,
|
||||
Content: "1",
|
||||
}
|
||||
if _, err = CreateCommentCtx(ctx, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return updateLabelCols(ctx, label, "num_issues", "num_closed_issue")
|
||||
}
|
||||
|
||||
// NewIssueLabel creates a new issue-label relation.
|
||||
func NewIssueLabel(issue *Issue, label *Label, doer *user_model.User) (err error) {
|
||||
if HasIssueLabel(db.DefaultContext, issue.ID, label.ID) {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if err = issue.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Do NOT add invalid labels
|
||||
if issue.RepoID != label.RepoID && issue.Repo.OwnerID != label.OrgID {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = newIssueLabel(ctx, issue, label, doer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
issue.Labels = nil
|
||||
if err = issue.LoadLabels(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// newIssueLabels add labels to an issue. It will check if the labels are valid for the issue
|
||||
func newIssueLabels(ctx context.Context, issue *Issue, labels []*Label, doer *user_model.User) (err error) {
|
||||
if err = issue.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, label := range labels {
|
||||
// Don't add already present labels and invalid labels
|
||||
if HasIssueLabel(ctx, issue.ID, label.ID) ||
|
||||
(label.RepoID != issue.RepoID && label.OrgID != issue.Repo.OwnerID) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err = newIssueLabel(ctx, issue, label, doer); err != nil {
|
||||
return fmt.Errorf("newIssueLabel: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewIssueLabels creates a list of issue-label relations.
|
||||
func NewIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) {
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if err = newIssueLabels(ctx, issue, labels, doer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
issue.Labels = nil
|
||||
if err = issue.LoadLabels(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
func deleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) {
|
||||
if count, err := db.DeleteByBean(ctx, &IssueLabel{
|
||||
IssueID: issue.ID,
|
||||
LabelID: label.ID,
|
||||
}); err != nil {
|
||||
return err
|
||||
} else if count == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = issue.LoadRepo(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
opts := &CreateCommentOptions{
|
||||
Type: CommentTypeLabel,
|
||||
Doer: doer,
|
||||
Repo: issue.Repo,
|
||||
Issue: issue,
|
||||
Label: label,
|
||||
}
|
||||
if _, err = CreateCommentCtx(ctx, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return updateLabelCols(ctx, label, "num_issues", "num_closed_issue")
|
||||
}
|
||||
|
||||
// DeleteIssueLabel deletes issue-label relation.
|
||||
func DeleteIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) error {
|
||||
if err := deleteIssueLabel(ctx, issue, label, doer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
issue.Labels = nil
|
||||
return issue.LoadLabels(ctx)
|
||||
}
|
||||
|
||||
// DeleteLabelsByRepoID deletes labels of some repository
|
||||
func DeleteLabelsByRepoID(ctx context.Context, repoID int64) error {
|
||||
deleteCond := builder.Select("id").From("label").Where(builder.Eq{"label.repo_id": repoID})
|
||||
|
||||
if _, err := db.GetEngine(ctx).In("label_id", deleteCond).
|
||||
Delete(&IssueLabel{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := db.DeleteByBean(ctx, &Label{RepoID: repoID})
|
||||
return err
|
||||
}
|
||||
|
||||
// CountOrphanedLabels return count of labels witch are broken and not accessible via ui anymore
|
||||
func CountOrphanedLabels() (int64, error) {
|
||||
noref, err := db.GetEngine(db.DefaultContext).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Count("label.id")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
norepo, err := db.GetEngine(db.DefaultContext).Table("label").
|
||||
Where(builder.And(
|
||||
builder.Gt{"repo_id": 0},
|
||||
builder.NotIn("repo_id", builder.Select("id").From("repository")),
|
||||
)).
|
||||
Count()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
noorg, err := db.GetEngine(db.DefaultContext).Table("label").
|
||||
Where(builder.And(
|
||||
builder.Gt{"org_id": 0},
|
||||
builder.NotIn("org_id", builder.Select("id").From("user")),
|
||||
)).
|
||||
Count()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return noref + norepo + noorg, nil
|
||||
}
|
||||
|
||||
// DeleteOrphanedLabels delete labels witch are broken and not accessible via ui anymore
|
||||
func DeleteOrphanedLabels() error {
|
||||
// delete labels with no reference
|
||||
if _, err := db.GetEngine(db.DefaultContext).Table("label").Where("repo_id=? AND org_id=?", 0, 0).Delete(new(Label)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete labels with none existing repos
|
||||
if _, err := db.GetEngine(db.DefaultContext).
|
||||
Where(builder.And(
|
||||
builder.Gt{"repo_id": 0},
|
||||
builder.NotIn("repo_id", builder.Select("id").From("repository")),
|
||||
)).
|
||||
Delete(Label{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// delete labels with none existing orgs
|
||||
if _, err := db.GetEngine(db.DefaultContext).
|
||||
Where(builder.And(
|
||||
builder.Gt{"org_id": 0},
|
||||
builder.NotIn("org_id", builder.Select("id").From("user")),
|
||||
)).
|
||||
Delete(Label{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CountOrphanedIssueLabels return count of IssueLabels witch have no label behind anymore
|
||||
func CountOrphanedIssueLabels() (int64, error) {
|
||||
return db.GetEngine(db.DefaultContext).Table("issue_label").
|
||||
NotIn("label_id", builder.Select("id").From("label")).
|
||||
Count()
|
||||
}
|
||||
|
||||
// DeleteOrphanedIssueLabels delete IssueLabels witch have no label behind anymore
|
||||
func DeleteOrphanedIssueLabels() error {
|
||||
_, err := db.GetEngine(db.DefaultContext).
|
||||
NotIn("label_id", builder.Select("id").From("label")).
|
||||
Delete(IssueLabel{})
|
||||
return err
|
||||
}
|
||||
|
||||
// CountIssueLabelWithOutsideLabels count label comments with outside label
|
||||
func CountIssueLabelWithOutsideLabels() (int64, error) {
|
||||
return db.GetEngine(db.DefaultContext).Where(builder.Expr("(label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)")).
|
||||
Table("issue_label").
|
||||
Join("inner", "label", "issue_label.label_id = label.id ").
|
||||
Join("inner", "issue", "issue.id = issue_label.issue_id ").
|
||||
Join("inner", "repository", "issue.repo_id = repository.id").
|
||||
Count(new(IssueLabel))
|
||||
}
|
||||
|
||||
// FixIssueLabelWithOutsideLabels fix label comments with outside label
|
||||
func FixIssueLabelWithOutsideLabels() (int64, error) {
|
||||
res, err := db.GetEngine(db.DefaultContext).Exec(`DELETE FROM issue_label WHERE issue_label.id IN (
|
||||
SELECT il_too.id FROM (
|
||||
SELECT il_too_too.id
|
||||
FROM issue_label AS il_too_too
|
||||
INNER JOIN label ON il_too_too.label_id = label.id
|
||||
INNER JOIN issue on issue.id = il_too_too.issue_id
|
||||
INNER JOIN repository on repository.id = issue.repo_id
|
||||
WHERE
|
||||
(label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)
|
||||
) AS il_too )`)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return res.RowsAffected()
|
||||
}
|
395
models/issues/label_test.go
Normal file
395
models/issues/label_test.go
Normal file
|
@ -0,0 +1,395 @@
|
|||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues_test
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
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"
|
||||
)
|
||||
|
||||
// TODO TestGetLabelTemplateFile
|
||||
|
||||
func TestLabel_CalOpenIssues(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label)
|
||||
label.CalOpenIssues()
|
||||
assert.EqualValues(t, 2, label.NumOpenIssues)
|
||||
}
|
||||
|
||||
func TestLabel_ForegroundColor(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label)
|
||||
assert.Equal(t, template.CSS("#000"), label.ForegroundColor())
|
||||
|
||||
label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2}).(*issues_model.Label)
|
||||
assert.Equal(t, template.CSS("#fff"), label.ForegroundColor())
|
||||
}
|
||||
|
||||
func TestNewLabels(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
labels := []*issues_model.Label{
|
||||
{RepoID: 2, Name: "labelName2", Color: "#123456"},
|
||||
{RepoID: 3, Name: "labelName3", Color: "#123"},
|
||||
{RepoID: 4, Name: "labelName4", Color: "ABCDEF"},
|
||||
{RepoID: 5, Name: "labelName5", Color: "DEF"},
|
||||
}
|
||||
assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: ""}))
|
||||
assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "#45G"}))
|
||||
assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "#12345G"}))
|
||||
assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "45G"}))
|
||||
assert.Error(t, issues_model.NewLabel(db.DefaultContext, &issues_model.Label{RepoID: 3, Name: "invalid Color", Color: "12345G"}))
|
||||
for _, label := range labels {
|
||||
unittest.AssertNotExistsBean(t, label)
|
||||
}
|
||||
assert.NoError(t, issues_model.NewLabels(labels...))
|
||||
for _, label := range labels {
|
||||
unittest.AssertExistsAndLoadBean(t, label, unittest.Cond("id = ?", label.ID))
|
||||
}
|
||||
unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{})
|
||||
}
|
||||
|
||||
func TestGetLabelByID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
label, err := issues_model.GetLabelByID(db.DefaultContext, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, label.ID)
|
||||
|
||||
_, err = issues_model.GetLabelByID(db.DefaultContext, unittest.NonexistentID)
|
||||
assert.True(t, issues_model.IsErrLabelNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetLabelInRepoByName(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
label, err := issues_model.GetLabelInRepoByName(db.DefaultContext, 1, "label1")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, label.ID)
|
||||
assert.Equal(t, "label1", label.Name)
|
||||
|
||||
_, err = issues_model.GetLabelInRepoByName(db.DefaultContext, 1, "")
|
||||
assert.True(t, issues_model.IsErrRepoLabelNotExist(err))
|
||||
|
||||
_, err = issues_model.GetLabelInRepoByName(db.DefaultContext, unittest.NonexistentID, "nonexistent")
|
||||
assert.True(t, issues_model.IsErrRepoLabelNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetLabelInRepoByNames(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
labelIDs, err := issues_model.GetLabelIDsInRepoByNames(1, []string{"label1", "label2"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, labelIDs, 2)
|
||||
|
||||
assert.Equal(t, int64(1), labelIDs[0])
|
||||
assert.Equal(t, int64(2), labelIDs[1])
|
||||
}
|
||||
|
||||
func TestGetLabelInRepoByNamesDiscardsNonExistentLabels(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
// label3 doesn't exists.. See labels.yml
|
||||
labelIDs, err := issues_model.GetLabelIDsInRepoByNames(1, []string{"label1", "label2", "label3"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, labelIDs, 2)
|
||||
|
||||
assert.Equal(t, int64(1), labelIDs[0])
|
||||
assert.Equal(t, int64(2), labelIDs[1])
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGetLabelInRepoByID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
label, err := issues_model.GetLabelInRepoByID(db.DefaultContext, 1, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, label.ID)
|
||||
|
||||
_, err = issues_model.GetLabelInRepoByID(db.DefaultContext, 1, -1)
|
||||
assert.True(t, issues_model.IsErrRepoLabelNotExist(err))
|
||||
|
||||
_, err = issues_model.GetLabelInRepoByID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)
|
||||
assert.True(t, issues_model.IsErrRepoLabelNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetLabelsInRepoByIDs(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
labels, err := issues_model.GetLabelsInRepoByIDs(1, []int64{1, 2, unittest.NonexistentID})
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, labels, 2) {
|
||||
assert.EqualValues(t, 1, labels[0].ID)
|
||||
assert.EqualValues(t, 2, labels[1].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLabelsByRepoID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
testSuccess := func(repoID int64, sortType string, expectedIssueIDs []int64) {
|
||||
labels, err := issues_model.GetLabelsByRepoID(db.DefaultContext, repoID, sortType, db.ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, labels, len(expectedIssueIDs))
|
||||
for i, label := range labels {
|
||||
assert.EqualValues(t, expectedIssueIDs[i], label.ID)
|
||||
}
|
||||
}
|
||||
testSuccess(1, "leastissues", []int64{2, 1})
|
||||
testSuccess(1, "mostissues", []int64{1, 2})
|
||||
testSuccess(1, "reversealphabetically", []int64{2, 1})
|
||||
testSuccess(1, "default", []int64{1, 2})
|
||||
}
|
||||
|
||||
// Org versions
|
||||
|
||||
func TestGetLabelInOrgByName(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
label, err := issues_model.GetLabelInOrgByName(db.DefaultContext, 3, "orglabel3")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 3, label.ID)
|
||||
assert.Equal(t, "orglabel3", label.Name)
|
||||
|
||||
_, err = issues_model.GetLabelInOrgByName(db.DefaultContext, 3, "")
|
||||
assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
|
||||
|
||||
_, err = issues_model.GetLabelInOrgByName(db.DefaultContext, 0, "orglabel3")
|
||||
assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
|
||||
|
||||
_, err = issues_model.GetLabelInOrgByName(db.DefaultContext, -1, "orglabel3")
|
||||
assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
|
||||
|
||||
_, err = issues_model.GetLabelInOrgByName(db.DefaultContext, unittest.NonexistentID, "nonexistent")
|
||||
assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetLabelInOrgByNames(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
labelIDs, err := issues_model.GetLabelIDsInOrgByNames(3, []string{"orglabel3", "orglabel4"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, labelIDs, 2)
|
||||
|
||||
assert.Equal(t, int64(3), labelIDs[0])
|
||||
assert.Equal(t, int64(4), labelIDs[1])
|
||||
}
|
||||
|
||||
func TestGetLabelInOrgByNamesDiscardsNonExistentLabels(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
// orglabel99 doesn't exists.. See labels.yml
|
||||
labelIDs, err := issues_model.GetLabelIDsInOrgByNames(3, []string{"orglabel3", "orglabel4", "orglabel99"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, labelIDs, 2)
|
||||
|
||||
assert.Equal(t, int64(3), labelIDs[0])
|
||||
assert.Equal(t, int64(4), labelIDs[1])
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGetLabelInOrgByID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
label, err := issues_model.GetLabelInOrgByID(db.DefaultContext, 3, 3)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 3, label.ID)
|
||||
|
||||
_, err = issues_model.GetLabelInOrgByID(db.DefaultContext, 3, -1)
|
||||
assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
|
||||
|
||||
_, err = issues_model.GetLabelInOrgByID(db.DefaultContext, 0, 3)
|
||||
assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
|
||||
|
||||
_, err = issues_model.GetLabelInOrgByID(db.DefaultContext, -1, 3)
|
||||
assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
|
||||
|
||||
_, err = issues_model.GetLabelInOrgByID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)
|
||||
assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetLabelsInOrgByIDs(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
labels, err := issues_model.GetLabelsInOrgByIDs(3, []int64{3, 4, unittest.NonexistentID})
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, labels, 2) {
|
||||
assert.EqualValues(t, 3, labels[0].ID)
|
||||
assert.EqualValues(t, 4, labels[1].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLabelsByOrgID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
testSuccess := func(orgID int64, sortType string, expectedIssueIDs []int64) {
|
||||
labels, err := issues_model.GetLabelsByOrgID(db.DefaultContext, orgID, sortType, db.ListOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, labels, len(expectedIssueIDs))
|
||||
for i, label := range labels {
|
||||
assert.EqualValues(t, expectedIssueIDs[i], label.ID)
|
||||
}
|
||||
}
|
||||
testSuccess(3, "leastissues", []int64{3, 4})
|
||||
testSuccess(3, "mostissues", []int64{4, 3})
|
||||
testSuccess(3, "reversealphabetically", []int64{4, 3})
|
||||
testSuccess(3, "default", []int64{3, 4})
|
||||
|
||||
var err error
|
||||
_, err = issues_model.GetLabelsByOrgID(db.DefaultContext, 0, "leastissues", db.ListOptions{})
|
||||
assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
|
||||
|
||||
_, err = issues_model.GetLabelsByOrgID(db.DefaultContext, -1, "leastissues", db.ListOptions{})
|
||||
assert.True(t, issues_model.IsErrOrgLabelNotExist(err))
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
func TestGetLabelsByIssueID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
labels, err := issues_model.GetLabelsByIssueID(db.DefaultContext, 1)
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, labels, 1) {
|
||||
assert.EqualValues(t, 1, labels[0].ID)
|
||||
}
|
||||
|
||||
labels, err = issues_model.GetLabelsByIssueID(db.DefaultContext, unittest.NonexistentID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, labels, 0)
|
||||
}
|
||||
|
||||
func TestUpdateLabel(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label)
|
||||
// make sure update wont overwrite it
|
||||
update := &issues_model.Label{
|
||||
ID: label.ID,
|
||||
Color: "#ffff00",
|
||||
Name: "newLabelName",
|
||||
Description: label.Description,
|
||||
}
|
||||
label.Color = update.Color
|
||||
label.Name = update.Name
|
||||
assert.NoError(t, issues_model.UpdateLabel(update))
|
||||
newLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label)
|
||||
assert.EqualValues(t, label.ID, newLabel.ID)
|
||||
assert.EqualValues(t, label.Color, newLabel.Color)
|
||||
assert.EqualValues(t, label.Name, newLabel.Name)
|
||||
assert.EqualValues(t, label.Description, newLabel.Description)
|
||||
unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{})
|
||||
}
|
||||
|
||||
func TestDeleteLabel(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label)
|
||||
assert.NoError(t, issues_model.DeleteLabel(label.RepoID, label.ID))
|
||||
unittest.AssertNotExistsBean(t, &issues_model.Label{ID: label.ID, RepoID: label.RepoID})
|
||||
|
||||
assert.NoError(t, issues_model.DeleteLabel(label.RepoID, label.ID))
|
||||
unittest.AssertNotExistsBean(t, &issues_model.Label{ID: label.ID})
|
||||
|
||||
assert.NoError(t, issues_model.DeleteLabel(unittest.NonexistentID, unittest.NonexistentID))
|
||||
unittest.CheckConsistencyFor(t, &issues_model.Label{}, &repo_model.Repository{})
|
||||
}
|
||||
|
||||
func TestHasIssueLabel(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
assert.True(t, issues_model.HasIssueLabel(db.DefaultContext, 1, 1))
|
||||
assert.False(t, issues_model.HasIssueLabel(db.DefaultContext, 1, 2))
|
||||
assert.False(t, issues_model.HasIssueLabel(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID))
|
||||
}
|
||||
|
||||
func TestNewIssueLabel(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2}).(*issues_model.Label)
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}).(*issues_model.Issue)
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
|
||||
|
||||
// add new IssueLabel
|
||||
prevNumIssues := label.NumIssues
|
||||
assert.NoError(t, issues_model.NewIssueLabel(issue, label, doer))
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label.ID})
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
|
||||
Type: issues_model.CommentTypeLabel,
|
||||
PosterID: doer.ID,
|
||||
IssueID: issue.ID,
|
||||
LabelID: label.ID,
|
||||
Content: "1",
|
||||
})
|
||||
label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2}).(*issues_model.Label)
|
||||
assert.EqualValues(t, prevNumIssues+1, label.NumIssues)
|
||||
|
||||
// re-add existing IssueLabel
|
||||
assert.NoError(t, issues_model.NewIssueLabel(issue, label, doer))
|
||||
unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.Label{})
|
||||
}
|
||||
|
||||
func TestNewIssueLabels(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
label1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label)
|
||||
label2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2}).(*issues_model.Label)
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 5}).(*issues_model.Issue)
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
|
||||
|
||||
assert.NoError(t, issues_model.NewIssueLabels(issue, []*issues_model.Label{label1, label2}, doer))
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label1.ID})
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
|
||||
Type: issues_model.CommentTypeLabel,
|
||||
PosterID: doer.ID,
|
||||
IssueID: issue.ID,
|
||||
LabelID: label1.ID,
|
||||
Content: "1",
|
||||
})
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: label1.ID})
|
||||
label1 = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}).(*issues_model.Label)
|
||||
assert.EqualValues(t, 3, label1.NumIssues)
|
||||
assert.EqualValues(t, 1, label1.NumClosedIssues)
|
||||
label2 = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2}).(*issues_model.Label)
|
||||
assert.EqualValues(t, 1, label2.NumIssues)
|
||||
assert.EqualValues(t, 1, label2.NumClosedIssues)
|
||||
|
||||
// corner case: test empty slice
|
||||
assert.NoError(t, issues_model.NewIssueLabels(issue, []*issues_model.Label{}, doer))
|
||||
|
||||
unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.Label{})
|
||||
}
|
||||
|
||||
func TestDeleteIssueLabel(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
testSuccess := func(labelID, issueID, doerID int64) {
|
||||
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID}).(*issues_model.Label)
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueID}).(*issues_model.Issue)
|
||||
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: doerID}).(*user_model.User)
|
||||
|
||||
expectedNumIssues := label.NumIssues
|
||||
expectedNumClosedIssues := label.NumClosedIssues
|
||||
if unittest.BeanExists(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID}) {
|
||||
expectedNumIssues--
|
||||
if issue.IsClosed {
|
||||
expectedNumClosedIssues--
|
||||
}
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext()
|
||||
defer committer.Close()
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, issues_model.DeleteIssueLabel(ctx, issue, label, doer))
|
||||
assert.NoError(t, committer.Commit())
|
||||
|
||||
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID})
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{
|
||||
Type: issues_model.CommentTypeLabel,
|
||||
PosterID: doerID,
|
||||
IssueID: issueID,
|
||||
LabelID: labelID,
|
||||
}, `content=""`)
|
||||
label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID}).(*issues_model.Label)
|
||||
assert.EqualValues(t, expectedNumIssues, label.NumIssues)
|
||||
assert.EqualValues(t, expectedNumClosedIssues, label.NumClosedIssues)
|
||||
}
|
||||
testSuccess(1, 1, 2)
|
||||
testSuccess(2, 5, 2)
|
||||
testSuccess(1, 1, 2) // delete non-existent IssueLabel
|
||||
|
||||
unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.Label{})
|
||||
}
|
|
@ -2,14 +2,20 @@
|
|||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues
|
||||
package issues_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
_ "code.gitea.io/gitea/models"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
_ "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
_ "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -17,14 +23,18 @@ func init() {
|
|||
setting.LoadForTest()
|
||||
}
|
||||
|
||||
func TestFixturesAreConsistent(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
unittest.CheckConsistencyFor(t,
|
||||
&issues_model.Issue{},
|
||||
&issues_model.PullRequest{},
|
||||
&issues_model.Milestone{},
|
||||
&issues_model.Label{},
|
||||
)
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m, &unittest.TestOptions{
|
||||
GiteaRootPath: filepath.Join("..", ".."),
|
||||
FixtureFiles: []string{
|
||||
"reaction.yml",
|
||||
"user.yml",
|
||||
"repository.yml",
|
||||
"milestone.yml",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -292,11 +292,17 @@ func DeleteMilestoneByRepoID(repoID, id int64) error {
|
|||
return err
|
||||
}
|
||||
|
||||
numMilestones, err := countRepoMilestones(ctx, repo.ID)
|
||||
numMilestones, err := CountMilestones(ctx, GetMilestonesOption{
|
||||
RepoID: repo.ID,
|
||||
State: api.StateAll,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
numClosedMilestones, err := countRepoClosedMilestones(ctx, repo.ID)
|
||||
numClosedMilestones, err := CountMilestones(ctx, GetMilestonesOption{
|
||||
RepoID: repo.ID,
|
||||
State: api.StateClosed,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -428,13 +434,6 @@ func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType s
|
|||
)
|
||||
}
|
||||
|
||||
// ____ _ _
|
||||
// / ___|| |_ __ _| |_ ___
|
||||
// \___ \| __/ _` | __/ __|
|
||||
// ___) | || (_| | |_\__ \
|
||||
// |____/ \__\__,_|\__|___/
|
||||
//
|
||||
|
||||
// MilestonesStats represents milestone statistic information.
|
||||
type MilestonesStats struct {
|
||||
OpenCount, ClosedCount int64
|
||||
|
@ -503,23 +502,13 @@ func GetMilestonesStatsByRepoCondAndKw(repoCond builder.Cond, keyword string) (*
|
|||
return stats, nil
|
||||
}
|
||||
|
||||
func countRepoMilestones(ctx context.Context, repoID int64) (int64, error) {
|
||||
// CountMilestones returns number of milestones in given repository with other options
|
||||
func CountMilestones(ctx context.Context, opts GetMilestonesOption) (int64, error) {
|
||||
return db.GetEngine(ctx).
|
||||
Where("repo_id=?", repoID).
|
||||
Where(opts.toCond()).
|
||||
Count(new(Milestone))
|
||||
}
|
||||
|
||||
func countRepoClosedMilestones(ctx context.Context, repoID int64) (int64, error) {
|
||||
return db.GetEngine(ctx).
|
||||
Where("repo_id=? AND is_closed=?", repoID, true).
|
||||
Count(new(Milestone))
|
||||
}
|
||||
|
||||
// CountRepoClosedMilestones returns number of closed milestones in given repository.
|
||||
func CountRepoClosedMilestones(repoID int64) (int64, error) {
|
||||
return countRepoClosedMilestones(db.DefaultContext, repoID)
|
||||
}
|
||||
|
||||
// CountMilestonesByRepoCond map from repo conditions to number of milestones matching the options`
|
||||
func CountMilestonesByRepoCond(repoCond builder.Cond, isClosed bool) (map[int64]int64, error) {
|
||||
sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed)
|
||||
|
|
|
@ -2,44 +2,46 @@
|
|||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues
|
||||
package issues_test
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func TestMilestone_State(t *testing.T) {
|
||||
assert.Equal(t, api.StateOpen, (&Milestone{IsClosed: false}).State())
|
||||
assert.Equal(t, api.StateClosed, (&Milestone{IsClosed: true}).State())
|
||||
assert.Equal(t, api.StateOpen, (&issues_model.Milestone{IsClosed: false}).State())
|
||||
assert.Equal(t, api.StateClosed, (&issues_model.Milestone{IsClosed: true}).State())
|
||||
}
|
||||
|
||||
func TestGetMilestoneByRepoID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
milestone, err := GetMilestoneByRepoID(db.DefaultContext, 1, 1)
|
||||
milestone, err := issues_model.GetMilestoneByRepoID(db.DefaultContext, 1, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, milestone.ID)
|
||||
assert.EqualValues(t, 1, milestone.RepoID)
|
||||
|
||||
_, err = GetMilestoneByRepoID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)
|
||||
assert.True(t, IsErrMilestoneNotExist(err))
|
||||
_, err = issues_model.GetMilestoneByRepoID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)
|
||||
assert.True(t, issues_model.IsErrMilestoneNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetMilestonesByRepoID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
test := func(repoID int64, state api.StateType) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}).(*repo_model.Repository)
|
||||
milestones, _, err := GetMilestones(GetMilestonesOption{
|
||||
milestones, _, err := issues_model.GetMilestones(issues_model.GetMilestonesOption{
|
||||
RepoID: repo.ID,
|
||||
State: state,
|
||||
})
|
||||
|
@ -76,7 +78,7 @@ func TestGetMilestonesByRepoID(t *testing.T) {
|
|||
test(3, api.StateClosed)
|
||||
test(3, api.StateAll)
|
||||
|
||||
milestones, _, err := GetMilestones(GetMilestonesOption{
|
||||
milestones, _, err := issues_model.GetMilestones(issues_model.GetMilestonesOption{
|
||||
RepoID: unittest.NonexistentID,
|
||||
State: api.StateOpen,
|
||||
})
|
||||
|
@ -87,9 +89,9 @@ func TestGetMilestonesByRepoID(t *testing.T) {
|
|||
func TestGetMilestones(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
|
||||
test := func(sortType string, sortCond func(*Milestone) int) {
|
||||
test := func(sortType string, sortCond func(*issues_model.Milestone) int) {
|
||||
for _, page := range []int{0, 1} {
|
||||
milestones, _, err := GetMilestones(GetMilestonesOption{
|
||||
milestones, _, err := issues_model.GetMilestones(issues_model.GetMilestonesOption{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: setting.UI.IssuePagingNum,
|
||||
|
@ -106,7 +108,7 @@ func TestGetMilestones(t *testing.T) {
|
|||
}
|
||||
assert.True(t, sort.IntsAreSorted(values))
|
||||
|
||||
milestones, _, err = GetMilestones(GetMilestonesOption{
|
||||
milestones, _, err = issues_model.GetMilestones(issues_model.GetMilestonesOption{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: setting.UI.IssuePagingNum,
|
||||
|
@ -125,22 +127,22 @@ func TestGetMilestones(t *testing.T) {
|
|||
assert.True(t, sort.IntsAreSorted(values))
|
||||
}
|
||||
}
|
||||
test("furthestduedate", func(milestone *Milestone) int {
|
||||
test("furthestduedate", func(milestone *issues_model.Milestone) int {
|
||||
return -int(milestone.DeadlineUnix)
|
||||
})
|
||||
test("leastcomplete", func(milestone *Milestone) int {
|
||||
test("leastcomplete", func(milestone *issues_model.Milestone) int {
|
||||
return milestone.Completeness
|
||||
})
|
||||
test("mostcomplete", func(milestone *Milestone) int {
|
||||
test("mostcomplete", func(milestone *issues_model.Milestone) int {
|
||||
return -milestone.Completeness
|
||||
})
|
||||
test("leastissues", func(milestone *Milestone) int {
|
||||
test("leastissues", func(milestone *issues_model.Milestone) int {
|
||||
return milestone.NumIssues
|
||||
})
|
||||
test("mostissues", func(milestone *Milestone) int {
|
||||
test("mostissues", func(milestone *issues_model.Milestone) int {
|
||||
return -milestone.NumIssues
|
||||
})
|
||||
test("soonestduedate", func(milestone *Milestone) int {
|
||||
test("soonestduedate", func(milestone *issues_model.Milestone) int {
|
||||
return int(milestone.DeadlineUnix)
|
||||
})
|
||||
}
|
||||
|
@ -149,7 +151,10 @@ func TestCountRepoMilestones(t *testing.T) {
|
|||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
test := func(repoID int64) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}).(*repo_model.Repository)
|
||||
count, err := countRepoMilestones(db.DefaultContext, repoID)
|
||||
count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
|
||||
RepoID: repoID,
|
||||
State: api.StateAll,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, repo.NumMilestones, count)
|
||||
}
|
||||
|
@ -157,7 +162,10 @@ func TestCountRepoMilestones(t *testing.T) {
|
|||
test(2)
|
||||
test(3)
|
||||
|
||||
count, err := countRepoMilestones(db.DefaultContext, unittest.NonexistentID)
|
||||
count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
|
||||
RepoID: unittest.NonexistentID,
|
||||
State: api.StateAll,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 0, count)
|
||||
}
|
||||
|
@ -166,7 +174,10 @@ func TestCountRepoClosedMilestones(t *testing.T) {
|
|||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
test := func(repoID int64) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}).(*repo_model.Repository)
|
||||
count, err := CountRepoClosedMilestones(repoID)
|
||||
count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
|
||||
RepoID: repoID,
|
||||
State: api.StateClosed,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, repo.NumClosedMilestones, count)
|
||||
}
|
||||
|
@ -174,7 +185,10 @@ func TestCountRepoClosedMilestones(t *testing.T) {
|
|||
test(2)
|
||||
test(3)
|
||||
|
||||
count, err := CountRepoClosedMilestones(unittest.NonexistentID)
|
||||
count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
|
||||
RepoID: unittest.NonexistentID,
|
||||
State: api.StateClosed,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 0, count)
|
||||
}
|
||||
|
@ -188,12 +202,12 @@ func TestCountMilestonesByRepoIDs(t *testing.T) {
|
|||
repo1OpenCount, repo1ClosedCount := milestonesCount(1)
|
||||
repo2OpenCount, repo2ClosedCount := milestonesCount(2)
|
||||
|
||||
openCounts, err := CountMilestonesByRepoCond(builder.In("repo_id", []int64{1, 2}), false)
|
||||
openCounts, err := issues_model.CountMilestonesByRepoCond(builder.In("repo_id", []int64{1, 2}), false)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, repo1OpenCount, openCounts[1])
|
||||
assert.EqualValues(t, repo2OpenCount, openCounts[2])
|
||||
|
||||
closedCounts, err := CountMilestonesByRepoCond(builder.In("repo_id", []int64{1, 2}), true)
|
||||
closedCounts, err := issues_model.CountMilestonesByRepoCond(builder.In("repo_id", []int64{1, 2}), true)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, repo1ClosedCount, closedCounts[1])
|
||||
assert.EqualValues(t, repo2ClosedCount, closedCounts[2])
|
||||
|
@ -203,9 +217,9 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
|
|||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository)
|
||||
test := func(sortType string, sortCond func(*Milestone) int) {
|
||||
test := func(sortType string, sortCond func(*issues_model.Milestone) int) {
|
||||
for _, page := range []int{0, 1} {
|
||||
openMilestones, err := GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, false, sortType)
|
||||
openMilestones, err := issues_model.GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, false, sortType)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, openMilestones, repo1.NumOpenMilestones+repo2.NumOpenMilestones)
|
||||
values := make([]int, len(openMilestones))
|
||||
|
@ -214,7 +228,7 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
|
|||
}
|
||||
assert.True(t, sort.IntsAreSorted(values))
|
||||
|
||||
closedMilestones, err := GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, true, sortType)
|
||||
closedMilestones, err := issues_model.GetMilestonesByRepoIDs([]int64{repo1.ID, repo2.ID}, page, true, sortType)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, closedMilestones, repo1.NumClosedMilestones+repo2.NumClosedMilestones)
|
||||
values = make([]int, len(closedMilestones))
|
||||
|
@ -224,22 +238,22 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
|
|||
assert.True(t, sort.IntsAreSorted(values))
|
||||
}
|
||||
}
|
||||
test("furthestduedate", func(milestone *Milestone) int {
|
||||
test("furthestduedate", func(milestone *issues_model.Milestone) int {
|
||||
return -int(milestone.DeadlineUnix)
|
||||
})
|
||||
test("leastcomplete", func(milestone *Milestone) int {
|
||||
test("leastcomplete", func(milestone *issues_model.Milestone) int {
|
||||
return milestone.Completeness
|
||||
})
|
||||
test("mostcomplete", func(milestone *Milestone) int {
|
||||
test("mostcomplete", func(milestone *issues_model.Milestone) int {
|
||||
return -milestone.Completeness
|
||||
})
|
||||
test("leastissues", func(milestone *Milestone) int {
|
||||
test("leastissues", func(milestone *issues_model.Milestone) int {
|
||||
return milestone.NumIssues
|
||||
})
|
||||
test("mostissues", func(milestone *Milestone) int {
|
||||
test("mostissues", func(milestone *issues_model.Milestone) int {
|
||||
return -milestone.NumIssues
|
||||
})
|
||||
test("soonestduedate", func(milestone *Milestone) int {
|
||||
test("soonestduedate", func(milestone *issues_model.Milestone) int {
|
||||
return int(milestone.DeadlineUnix)
|
||||
})
|
||||
}
|
||||
|
@ -249,7 +263,7 @@ func TestGetMilestonesStats(t *testing.T) {
|
|||
|
||||
test := func(repoID int64) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}).(*repo_model.Repository)
|
||||
stats, err := GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"repo_id": repoID}))
|
||||
stats, err := issues_model.GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"repo_id": repoID}))
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, repo.NumMilestones-repo.NumClosedMilestones, stats.OpenCount)
|
||||
assert.EqualValues(t, repo.NumClosedMilestones, stats.ClosedCount)
|
||||
|
@ -258,7 +272,7 @@ func TestGetMilestonesStats(t *testing.T) {
|
|||
test(2)
|
||||
test(3)
|
||||
|
||||
stats, err := GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"repo_id": unittest.NonexistentID}))
|
||||
stats, err := issues_model.GetMilestonesStatsByRepoCond(builder.And(builder.Eq{"repo_id": unittest.NonexistentID}))
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 0, stats.OpenCount)
|
||||
assert.EqualValues(t, 0, stats.ClosedCount)
|
||||
|
@ -266,8 +280,75 @@ func TestGetMilestonesStats(t *testing.T) {
|
|||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}).(*repo_model.Repository)
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}).(*repo_model.Repository)
|
||||
|
||||
milestoneStats, err := GetMilestonesStatsByRepoCond(builder.In("repo_id", []int64{repo1.ID, repo2.ID}))
|
||||
milestoneStats, err := issues_model.GetMilestonesStatsByRepoCond(builder.In("repo_id", []int64{repo1.ID, repo2.ID}))
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, repo1.NumOpenMilestones+repo2.NumOpenMilestones, milestoneStats.OpenCount)
|
||||
assert.EqualValues(t, repo1.NumClosedMilestones+repo2.NumClosedMilestones, milestoneStats.ClosedCount)
|
||||
}
|
||||
|
||||
func TestNewMilestone(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
milestone := &issues_model.Milestone{
|
||||
RepoID: 1,
|
||||
Name: "milestoneName",
|
||||
Content: "milestoneContent",
|
||||
}
|
||||
|
||||
assert.NoError(t, issues_model.NewMilestone(milestone))
|
||||
unittest.AssertExistsAndLoadBean(t, milestone)
|
||||
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{})
|
||||
}
|
||||
|
||||
func TestChangeMilestoneStatus(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone)
|
||||
|
||||
assert.NoError(t, issues_model.ChangeMilestoneStatus(milestone, true))
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}, "is_closed=1")
|
||||
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{})
|
||||
|
||||
assert.NoError(t, issues_model.ChangeMilestoneStatus(milestone, false))
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}, "is_closed=0")
|
||||
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: milestone.RepoID}, &issues_model.Milestone{})
|
||||
}
|
||||
|
||||
func TestDeleteMilestoneByRepoID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
assert.NoError(t, issues_model.DeleteMilestoneByRepoID(1, 1))
|
||||
unittest.AssertNotExistsBean(t, &issues_model.Milestone{ID: 1})
|
||||
unittest.CheckConsistencyFor(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
assert.NoError(t, issues_model.DeleteMilestoneByRepoID(unittest.NonexistentID, unittest.NonexistentID))
|
||||
}
|
||||
|
||||
func TestUpdateMilestone(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone)
|
||||
milestone.Name = " newMilestoneName "
|
||||
milestone.Content = "newMilestoneContent"
|
||||
assert.NoError(t, issues_model.UpdateMilestone(milestone, milestone.IsClosed))
|
||||
milestone = unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}).(*issues_model.Milestone)
|
||||
assert.EqualValues(t, "newMilestoneName", milestone.Name)
|
||||
unittest.CheckConsistencyFor(t, &issues_model.Milestone{})
|
||||
}
|
||||
|
||||
func TestUpdateMilestoneCounters(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{MilestoneID: 1},
|
||||
"is_closed=0").(*issues_model.Issue)
|
||||
|
||||
issue.IsClosed = true
|
||||
issue.ClosedUnix = timeutil.TimeStampNow()
|
||||
_, err := db.GetEngine(db.DefaultContext).ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID))
|
||||
unittest.CheckConsistencyFor(t, &issues_model.Milestone{})
|
||||
|
||||
issue.IsClosed = false
|
||||
issue.ClosedUnix = 0
|
||||
_, err = db.GetEngine(db.DefaultContext).ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID))
|
||||
unittest.CheckConsistencyFor(t, &issues_model.Milestone{})
|
||||
}
|
||||
|
|
838
models/issues/pull.go
Normal file
838
models/issues/pull.go
Normal file
|
@ -0,0 +1,838 @@
|
|||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
pull_model "code.gitea.io/gitea/models/pull"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ErrPullRequestNotExist represents a "PullRequestNotExist" kind of error.
|
||||
type ErrPullRequestNotExist struct {
|
||||
ID int64
|
||||
IssueID int64
|
||||
HeadRepoID int64
|
||||
BaseRepoID int64
|
||||
HeadBranch string
|
||||
BaseBranch string
|
||||
}
|
||||
|
||||
// IsErrPullRequestNotExist checks if an error is a ErrPullRequestNotExist.
|
||||
func IsErrPullRequestNotExist(err error) bool {
|
||||
_, ok := err.(ErrPullRequestNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrPullRequestNotExist) Error() string {
|
||||
return fmt.Sprintf("pull request does not exist [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]",
|
||||
err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch)
|
||||
}
|
||||
|
||||
// ErrPullRequestAlreadyExists represents a "PullRequestAlreadyExists"-error
|
||||
type ErrPullRequestAlreadyExists struct {
|
||||
ID int64
|
||||
IssueID int64
|
||||
HeadRepoID int64
|
||||
BaseRepoID int64
|
||||
HeadBranch string
|
||||
BaseBranch string
|
||||
}
|
||||
|
||||
// IsErrPullRequestAlreadyExists checks if an error is a ErrPullRequestAlreadyExists.
|
||||
func IsErrPullRequestAlreadyExists(err error) bool {
|
||||
_, ok := err.(ErrPullRequestAlreadyExists)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Error does pretty-printing :D
|
||||
func (err ErrPullRequestAlreadyExists) Error() string {
|
||||
return fmt.Sprintf("pull request already exists for these targets [id: %d, issue_id: %d, head_repo_id: %d, base_repo_id: %d, head_branch: %s, base_branch: %s]",
|
||||
err.ID, err.IssueID, err.HeadRepoID, err.BaseRepoID, err.HeadBranch, err.BaseBranch)
|
||||
}
|
||||
|
||||
// ErrPullRequestHeadRepoMissing represents a "ErrPullRequestHeadRepoMissing" error
|
||||
type ErrPullRequestHeadRepoMissing struct {
|
||||
ID int64
|
||||
HeadRepoID int64
|
||||
}
|
||||
|
||||
// IsErrErrPullRequestHeadRepoMissing checks if an error is a ErrPullRequestHeadRepoMissing.
|
||||
func IsErrErrPullRequestHeadRepoMissing(err error) bool {
|
||||
_, ok := err.(ErrPullRequestHeadRepoMissing)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Error does pretty-printing :D
|
||||
func (err ErrPullRequestHeadRepoMissing) Error() string {
|
||||
return fmt.Sprintf("pull request head repo missing [id: %d, head_repo_id: %d]",
|
||||
err.ID, err.HeadRepoID)
|
||||
}
|
||||
|
||||
// ErrPullWasClosed is used close a closed pull request
|
||||
type ErrPullWasClosed struct {
|
||||
ID int64
|
||||
Index int64
|
||||
}
|
||||
|
||||
// IsErrPullWasClosed checks if an error is a ErrErrPullWasClosed.
|
||||
func IsErrPullWasClosed(err error) bool {
|
||||
_, ok := err.(ErrPullWasClosed)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrPullWasClosed) Error() string {
|
||||
return fmt.Sprintf("Pull request [%d] %d was already closed", err.ID, err.Index)
|
||||
}
|
||||
|
||||
// PullRequestType defines pull request type
|
||||
type PullRequestType int
|
||||
|
||||
// Enumerate all the pull request types
|
||||
const (
|
||||
PullRequestGitea PullRequestType = iota
|
||||
PullRequestGit
|
||||
)
|
||||
|
||||
// PullRequestStatus defines pull request status
|
||||
type PullRequestStatus int
|
||||
|
||||
// Enumerate all the pull request status
|
||||
const (
|
||||
PullRequestStatusConflict PullRequestStatus = iota
|
||||
PullRequestStatusChecking
|
||||
PullRequestStatusMergeable
|
||||
PullRequestStatusManuallyMerged
|
||||
PullRequestStatusError
|
||||
PullRequestStatusEmpty
|
||||
)
|
||||
|
||||
// PullRequestFlow the flow of pull request
|
||||
type PullRequestFlow int
|
||||
|
||||
const (
|
||||
// PullRequestFlowGithub github flow from head branch to base branch
|
||||
PullRequestFlowGithub PullRequestFlow = iota
|
||||
// PullRequestFlowAGit Agit flow pull request, head branch is not exist
|
||||
PullRequestFlowAGit
|
||||
)
|
||||
|
||||
// PullRequest represents relation between pull request and repositories.
|
||||
type PullRequest struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Type PullRequestType
|
||||
Status PullRequestStatus
|
||||
ConflictedFiles []string `xorm:"TEXT JSON"`
|
||||
CommitsAhead int
|
||||
CommitsBehind int
|
||||
|
||||
ChangedProtectedFiles []string `xorm:"TEXT JSON"`
|
||||
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
Issue *Issue `xorm:"-"`
|
||||
Index int64
|
||||
|
||||
HeadRepoID int64 `xorm:"INDEX"`
|
||||
HeadRepo *repo_model.Repository `xorm:"-"`
|
||||
BaseRepoID int64 `xorm:"INDEX"`
|
||||
BaseRepo *repo_model.Repository `xorm:"-"`
|
||||
HeadBranch string
|
||||
HeadCommitID string `xorm:"-"`
|
||||
BaseBranch string
|
||||
ProtectedBranch *git_model.ProtectedBranch `xorm:"-"`
|
||||
MergeBase string `xorm:"VARCHAR(40)"`
|
||||
AllowMaintainerEdit bool `xorm:"NOT NULL DEFAULT false"`
|
||||
|
||||
HasMerged bool `xorm:"INDEX"`
|
||||
MergedCommitID string `xorm:"VARCHAR(40)"`
|
||||
MergerID int64 `xorm:"INDEX"`
|
||||
Merger *user_model.User `xorm:"-"`
|
||||
MergedUnix timeutil.TimeStamp `xorm:"updated INDEX"`
|
||||
|
||||
isHeadRepoLoaded bool `xorm:"-"`
|
||||
|
||||
Flow PullRequestFlow `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(PullRequest))
|
||||
}
|
||||
|
||||
// DeletePullsByBaseRepoID deletes all pull requests by the base repository ID
|
||||
func DeletePullsByBaseRepoID(ctx context.Context, repoID int64) error {
|
||||
deleteCond := builder.Select("id").From("pull_request").Where(builder.Eq{"pull_request.base_repo_id": repoID})
|
||||
|
||||
// Delete scheduled auto merges
|
||||
if _, err := db.GetEngine(ctx).In("pull_id", deleteCond).
|
||||
Delete(&pull_model.AutoMerge{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete review states
|
||||
if _, err := db.GetEngine(ctx).In("pull_id", deleteCond).
|
||||
Delete(&pull_model.ReviewState{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := db.DeleteByBean(ctx, &PullRequest{BaseRepoID: repoID})
|
||||
return err
|
||||
}
|
||||
|
||||
// MustHeadUserName returns the HeadRepo's username if failed return blank
|
||||
func (pr *PullRequest) MustHeadUserName() string {
|
||||
if err := pr.LoadHeadRepo(); err != nil {
|
||||
if !repo_model.IsErrRepoNotExist(err) {
|
||||
log.Error("LoadHeadRepo: %v", err)
|
||||
} else {
|
||||
log.Warn("LoadHeadRepo %d but repository does not exist: %v", pr.HeadRepoID, err)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
if pr.HeadRepo == nil {
|
||||
return ""
|
||||
}
|
||||
return pr.HeadRepo.OwnerName
|
||||
}
|
||||
|
||||
// Note: don't try to get Issue because will end up recursive querying.
|
||||
func (pr *PullRequest) loadAttributes(ctx context.Context) (err error) {
|
||||
if pr.HasMerged && pr.Merger == nil {
|
||||
pr.Merger, err = user_model.GetUserByIDCtx(ctx, pr.MergerID)
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
pr.MergerID = -1
|
||||
pr.Merger = user_model.NewGhostUser()
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("getUserByID [%d]: %v", pr.MergerID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAttributes loads pull request attributes from database
|
||||
func (pr *PullRequest) LoadAttributes() error {
|
||||
return pr.loadAttributes(db.DefaultContext)
|
||||
}
|
||||
|
||||
// LoadHeadRepoCtx loads the head repository
|
||||
func (pr *PullRequest) LoadHeadRepoCtx(ctx context.Context) (err error) {
|
||||
if !pr.isHeadRepoLoaded && pr.HeadRepo == nil && pr.HeadRepoID > 0 {
|
||||
if pr.HeadRepoID == pr.BaseRepoID {
|
||||
if pr.BaseRepo != nil {
|
||||
pr.HeadRepo = pr.BaseRepo
|
||||
return nil
|
||||
} else if pr.Issue != nil && pr.Issue.Repo != nil {
|
||||
pr.HeadRepo = pr.Issue.Repo
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
pr.HeadRepo, err = repo_model.GetRepositoryByIDCtx(ctx, pr.HeadRepoID)
|
||||
if err != nil && !repo_model.IsErrRepoNotExist(err) { // Head repo maybe deleted, but it should still work
|
||||
return fmt.Errorf("getRepositoryByID(head): %v", err)
|
||||
}
|
||||
pr.isHeadRepoLoaded = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadHeadRepo loads the head repository
|
||||
func (pr *PullRequest) LoadHeadRepo() error {
|
||||
return pr.LoadHeadRepoCtx(db.DefaultContext)
|
||||
}
|
||||
|
||||
// LoadBaseRepo loads the target repository
|
||||
func (pr *PullRequest) LoadBaseRepo() error {
|
||||
return pr.LoadBaseRepoCtx(db.DefaultContext)
|
||||
}
|
||||
|
||||
// LoadBaseRepoCtx loads the target repository
|
||||
func (pr *PullRequest) LoadBaseRepoCtx(ctx context.Context) (err error) {
|
||||
if pr.BaseRepo != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if pr.HeadRepoID == pr.BaseRepoID && pr.HeadRepo != nil {
|
||||
pr.BaseRepo = pr.HeadRepo
|
||||
return nil
|
||||
}
|
||||
|
||||
if pr.Issue != nil && pr.Issue.Repo != nil {
|
||||
pr.BaseRepo = pr.Issue.Repo
|
||||
return nil
|
||||
}
|
||||
|
||||
pr.BaseRepo, err = repo_model.GetRepositoryByIDCtx(ctx, pr.BaseRepoID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("repo_model.GetRepositoryByID(base): %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadIssue loads issue information from database
|
||||
func (pr *PullRequest) LoadIssue() (err error) {
|
||||
return pr.LoadIssueCtx(db.DefaultContext)
|
||||
}
|
||||
|
||||
// LoadIssueCtx loads issue information from database
|
||||
func (pr *PullRequest) LoadIssueCtx(ctx context.Context) (err error) {
|
||||
if pr.Issue != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
pr.Issue, err = GetIssueByID(ctx, pr.IssueID)
|
||||
if err == nil {
|
||||
pr.Issue.PullRequest = pr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// LoadProtectedBranch loads the protected branch of the base branch
|
||||
func (pr *PullRequest) LoadProtectedBranch() (err error) {
|
||||
return pr.LoadProtectedBranchCtx(db.DefaultContext)
|
||||
}
|
||||
|
||||
// LoadProtectedBranchCtx loads the protected branch of the base branch
|
||||
func (pr *PullRequest) LoadProtectedBranchCtx(ctx context.Context) (err error) {
|
||||
if pr.ProtectedBranch == nil {
|
||||
if pr.BaseRepo == nil {
|
||||
if pr.BaseRepoID == 0 {
|
||||
return nil
|
||||
}
|
||||
pr.BaseRepo, err = repo_model.GetRepositoryByIDCtx(ctx, pr.BaseRepoID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
pr.ProtectedBranch, err = git_model.GetProtectedBranchBy(ctx, pr.BaseRepo.ID, pr.BaseBranch)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ReviewCount represents a count of Reviews
|
||||
type ReviewCount struct {
|
||||
IssueID int64
|
||||
Type ReviewType
|
||||
Count int64
|
||||
}
|
||||
|
||||
// GetApprovalCounts returns the approval counts by type
|
||||
// FIXME: Only returns official counts due to double counting of non-official counts
|
||||
func (pr *PullRequest) GetApprovalCounts(ctx context.Context) ([]*ReviewCount, error) {
|
||||
rCounts := make([]*ReviewCount, 0, 6)
|
||||
sess := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID)
|
||||
return rCounts, sess.Select("issue_id, type, count(id) as `count`").Where("official = ? AND dismissed = ?", true, false).GroupBy("issue_id, type").Table("review").Find(&rCounts)
|
||||
}
|
||||
|
||||
// GetApprovers returns the approvers of the pull request
|
||||
func (pr *PullRequest) GetApprovers() string {
|
||||
stringBuilder := strings.Builder{}
|
||||
if err := pr.getReviewedByLines(&stringBuilder); err != nil {
|
||||
log.Error("Unable to getReviewedByLines: Error: %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return stringBuilder.String()
|
||||
}
|
||||
|
||||
func (pr *PullRequest) getReviewedByLines(writer io.Writer) error {
|
||||
maxReviewers := setting.Repository.PullRequest.DefaultMergeMessageMaxApprovers
|
||||
|
||||
if maxReviewers == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
// Note: This doesn't page as we only expect a very limited number of reviews
|
||||
reviews, err := FindReviews(ctx, FindReviewOptions{
|
||||
Type: ReviewTypeApprove,
|
||||
IssueID: pr.IssueID,
|
||||
OfficialOnly: setting.Repository.PullRequest.DefaultMergeMessageOfficialApproversOnly,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("Unable to FindReviews for PR ID %d: %v", pr.ID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
reviewersWritten := 0
|
||||
|
||||
for _, review := range reviews {
|
||||
if maxReviewers > 0 && reviewersWritten > maxReviewers {
|
||||
break
|
||||
}
|
||||
|
||||
if err := review.loadReviewer(ctx); err != nil && !user_model.IsErrUserNotExist(err) {
|
||||
log.Error("Unable to LoadReviewer[%d] for PR ID %d : %v", review.ReviewerID, pr.ID, err)
|
||||
return err
|
||||
} else if review.Reviewer == nil {
|
||||
continue
|
||||
}
|
||||
if _, err := writer.Write([]byte("Reviewed-by: ")); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := writer.Write([]byte(review.Reviewer.NewGitSig().String())); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := writer.Write([]byte{'\n'}); err != nil {
|
||||
return err
|
||||
}
|
||||
reviewersWritten++
|
||||
}
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// GetGitRefName returns git ref for hidden pull request branch
|
||||
func (pr *PullRequest) GetGitRefName() string {
|
||||
return fmt.Sprintf("%s%d/head", git.PullPrefix, pr.Index)
|
||||
}
|
||||
|
||||
// IsChecking returns true if this pull request is still checking conflict.
|
||||
func (pr *PullRequest) IsChecking() bool {
|
||||
return pr.Status == PullRequestStatusChecking
|
||||
}
|
||||
|
||||
// CanAutoMerge returns true if this pull request can be merged automatically.
|
||||
func (pr *PullRequest) CanAutoMerge() bool {
|
||||
return pr.Status == PullRequestStatusMergeable
|
||||
}
|
||||
|
||||
// IsEmpty returns true if this pull request is empty.
|
||||
func (pr *PullRequest) IsEmpty() bool {
|
||||
return pr.Status == PullRequestStatusEmpty
|
||||
}
|
||||
|
||||
// SetMerged sets a pull request to merged and closes the corresponding issue
|
||||
func (pr *PullRequest) SetMerged(ctx context.Context) (bool, error) {
|
||||
if pr.HasMerged {
|
||||
return false, fmt.Errorf("PullRequest[%d] already merged", pr.Index)
|
||||
}
|
||||
if pr.MergedCommitID == "" || pr.MergedUnix == 0 || pr.Merger == nil {
|
||||
return false, fmt.Errorf("Unable to merge PullRequest[%d], some required fields are empty", pr.Index)
|
||||
}
|
||||
|
||||
pr.HasMerged = true
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
if _, err := sess.Exec("UPDATE `issue` SET `repo_id` = `repo_id` WHERE `id` = ?", pr.IssueID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if _, err := sess.Exec("UPDATE `pull_request` SET `issue_id` = `issue_id` WHERE `id` = ?", pr.ID); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
pr.Issue = nil
|
||||
if err := pr.LoadIssueCtx(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if tmpPr, err := GetPullRequestByID(ctx, pr.ID); err != nil {
|
||||
return false, err
|
||||
} else if tmpPr.HasMerged {
|
||||
if pr.Issue.IsClosed {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("PullRequest[%d] already merged but it's associated issue [%d] is not closed", pr.Index, pr.IssueID)
|
||||
} else if pr.Issue.IsClosed {
|
||||
return false, fmt.Errorf("PullRequest[%d] already closed", pr.Index)
|
||||
}
|
||||
|
||||
if err := pr.Issue.LoadRepo(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if err := pr.Issue.Repo.GetOwner(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if _, err := changeIssueStatus(ctx, pr.Issue, pr.Merger, true, true); err != nil {
|
||||
return false, fmt.Errorf("Issue.changeStatus: %v", err)
|
||||
}
|
||||
|
||||
// reset the conflicted files as there cannot be any if we're merged
|
||||
pr.ConflictedFiles = []string{}
|
||||
|
||||
// We need to save all of the data used to compute this merge as it may have already been changed by TestPatch. FIXME: need to set some state to prevent TestPatch from running whilst we are merging.
|
||||
if _, err := sess.Where("id = ?", pr.ID).Cols("has_merged, status, merge_base, merged_commit_id, merger_id, merged_unix, conflicted_files").Update(pr); err != nil {
|
||||
return false, fmt.Errorf("Failed to update pr[%d]: %v", pr.ID, err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// NewPullRequest creates new pull request with labels for repository.
|
||||
func NewPullRequest(outerCtx context.Context, repo *repo_model.Repository, issue *Issue, labelIDs []int64, uuids []string, pr *PullRequest) (err error) {
|
||||
idx, err := db.GetNextResourceIndex("issue_index", repo.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate pull request index failed: %v", err)
|
||||
}
|
||||
|
||||
issue.Index = idx
|
||||
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
ctx.WithContext(outerCtx)
|
||||
|
||||
if err = NewIssueWithIndex(ctx, issue.Poster, NewIssueOptions{
|
||||
Repo: repo,
|
||||
Issue: issue,
|
||||
LabelIDs: labelIDs,
|
||||
Attachments: uuids,
|
||||
IsPull: true,
|
||||
}); err != nil {
|
||||
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) || IsErrNewIssueInsert(err) {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("newIssue: %v", err)
|
||||
}
|
||||
|
||||
pr.Index = issue.Index
|
||||
pr.BaseRepo = repo
|
||||
pr.IssueID = issue.ID
|
||||
if err = db.Insert(ctx, pr); err != nil {
|
||||
return fmt.Errorf("insert pull repo: %v", err)
|
||||
}
|
||||
|
||||
if err = committer.Commit(); err != nil {
|
||||
return fmt.Errorf("Commit: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUnmergedPullRequest returns a pull request that is open and has not been merged
|
||||
// by given head/base and repo/branch.
|
||||
func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch string, flow PullRequestFlow) (*PullRequest, error) {
|
||||
pr := new(PullRequest)
|
||||
has, err := db.GetEngine(db.DefaultContext).
|
||||
Where("head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND flow = ? AND issue.is_closed=?",
|
||||
headRepoID, headBranch, baseRepoID, baseBranch, false, flow, false).
|
||||
Join("INNER", "issue", "issue.id=pull_request.issue_id").
|
||||
Get(pr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrPullRequestNotExist{0, 0, headRepoID, baseRepoID, headBranch, baseBranch}
|
||||
}
|
||||
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
// GetLatestPullRequestByHeadInfo returns the latest pull request (regardless of its status)
|
||||
// by given head information (repo and branch).
|
||||
func GetLatestPullRequestByHeadInfo(repoID int64, branch string) (*PullRequest, error) {
|
||||
pr := new(PullRequest)
|
||||
has, err := db.GetEngine(db.DefaultContext).
|
||||
Where("head_repo_id = ? AND head_branch = ? AND flow = ?", repoID, branch, PullRequestFlowGithub).
|
||||
OrderBy("id DESC").
|
||||
Get(pr)
|
||||
if !has {
|
||||
return nil, err
|
||||
}
|
||||
return pr, err
|
||||
}
|
||||
|
||||
// GetPullRequestByIndex returns a pull request by the given index
|
||||
func GetPullRequestByIndex(ctx context.Context, repoID, index int64) (*PullRequest, error) {
|
||||
if index < 1 {
|
||||
return nil, ErrPullRequestNotExist{}
|
||||
}
|
||||
pr := &PullRequest{
|
||||
BaseRepoID: repoID,
|
||||
Index: index,
|
||||
}
|
||||
|
||||
has, err := db.GetEngine(ctx).Get(pr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrPullRequestNotExist{0, 0, 0, repoID, "", ""}
|
||||
}
|
||||
|
||||
if err = pr.loadAttributes(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = pr.LoadIssueCtx(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
// GetPullRequestByID returns a pull request by given ID.
|
||||
func GetPullRequestByID(ctx context.Context, id int64) (*PullRequest, error) {
|
||||
pr := new(PullRequest)
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(pr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrPullRequestNotExist{id, 0, 0, 0, "", ""}
|
||||
}
|
||||
return pr, pr.loadAttributes(ctx)
|
||||
}
|
||||
|
||||
// GetPullRequestByIssueIDWithNoAttributes returns pull request with no attributes loaded by given issue ID.
|
||||
func GetPullRequestByIssueIDWithNoAttributes(issueID int64) (*PullRequest, error) {
|
||||
var pr PullRequest
|
||||
has, err := db.GetEngine(db.DefaultContext).Where("issue_id = ?", issueID).Get(&pr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""}
|
||||
}
|
||||
return &pr, nil
|
||||
}
|
||||
|
||||
// GetPullRequestByIssueID returns pull request by given issue ID.
|
||||
func GetPullRequestByIssueID(ctx context.Context, issueID int64) (*PullRequest, error) {
|
||||
pr := &PullRequest{
|
||||
IssueID: issueID,
|
||||
}
|
||||
has, err := db.GetByBean(ctx, pr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""}
|
||||
}
|
||||
return pr, pr.loadAttributes(ctx)
|
||||
}
|
||||
|
||||
// GetAllUnmergedAgitPullRequestByPoster get all unmerged agit flow pull request
|
||||
// By poster id.
|
||||
func GetAllUnmergedAgitPullRequestByPoster(uid int64) ([]*PullRequest, error) {
|
||||
pulls := make([]*PullRequest, 0, 10)
|
||||
|
||||
err := db.GetEngine(db.DefaultContext).
|
||||
Where("has_merged=? AND flow = ? AND issue.is_closed=? AND issue.poster_id=?",
|
||||
false, PullRequestFlowAGit, false, uid).
|
||||
Join("INNER", "issue", "issue.id=pull_request.issue_id").
|
||||
Find(&pulls)
|
||||
|
||||
return pulls, err
|
||||
}
|
||||
|
||||
// Update updates all fields of pull request.
|
||||
func (pr *PullRequest) Update() error {
|
||||
_, err := db.GetEngine(db.DefaultContext).ID(pr.ID).AllCols().Update(pr)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateCols updates specific fields of pull request.
|
||||
func (pr *PullRequest) UpdateCols(cols ...string) error {
|
||||
_, err := db.GetEngine(db.DefaultContext).ID(pr.ID).Cols(cols...).Update(pr)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateColsIfNotMerged updates specific fields of a pull request if it has not been merged
|
||||
func (pr *PullRequest) UpdateColsIfNotMerged(cols ...string) error {
|
||||
_, err := db.GetEngine(db.DefaultContext).Where("id = ? AND has_merged = ?", pr.ID, false).Cols(cols...).Update(pr)
|
||||
return err
|
||||
}
|
||||
|
||||
// IsWorkInProgress determine if the Pull Request is a Work In Progress by its title
|
||||
func (pr *PullRequest) IsWorkInProgress() bool {
|
||||
if err := pr.LoadIssue(); err != nil {
|
||||
log.Error("LoadIssue: %v", err)
|
||||
return false
|
||||
}
|
||||
return HasWorkInProgressPrefix(pr.Issue.Title)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// IsFilesConflicted determines if the Pull Request has changes conflicting with the target branch.
|
||||
func (pr *PullRequest) IsFilesConflicted() bool {
|
||||
return len(pr.ConflictedFiles) > 0
|
||||
}
|
||||
|
||||
// GetWorkInProgressPrefix returns the prefix used to mark the pull request as a work in progress.
|
||||
// It returns an empty string when none were found
|
||||
func (pr *PullRequest) GetWorkInProgressPrefix() string {
|
||||
if err := pr.LoadIssue(); err != nil {
|
||||
log.Error("LoadIssue: %v", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, prefix := range setting.Repository.PullRequest.WorkInProgressPrefixes {
|
||||
if strings.HasPrefix(strings.ToUpper(pr.Issue.Title), strings.ToUpper(prefix)) {
|
||||
return pr.Issue.Title[0:len(prefix)]
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// UpdateCommitDivergence update Divergence of a pull request
|
||||
func (pr *PullRequest) UpdateCommitDivergence(ctx context.Context, ahead, behind int) error {
|
||||
if pr.ID == 0 {
|
||||
return fmt.Errorf("pull ID is 0")
|
||||
}
|
||||
pr.CommitsAhead = ahead
|
||||
pr.CommitsBehind = behind
|
||||
_, err := db.GetEngine(ctx).ID(pr.ID).Cols("commits_ahead", "commits_behind").Update(pr)
|
||||
return err
|
||||
}
|
||||
|
||||
// IsSameRepo returns true if base repo and head repo is the same
|
||||
func (pr *PullRequest) IsSameRepo() bool {
|
||||
return pr.BaseRepoID == pr.HeadRepoID
|
||||
}
|
||||
|
||||
// GetPullRequestsByHeadBranch returns all prs by head branch
|
||||
// Since there could be multiple prs with the same head branch, this function returns a slice of prs
|
||||
func GetPullRequestsByHeadBranch(ctx context.Context, headBranch string, headRepoID int64) ([]*PullRequest, error) {
|
||||
log.Trace("GetPullRequestsByHeadBranch: headBranch: '%s', headRepoID: '%d'", headBranch, headRepoID)
|
||||
prs := make([]*PullRequest, 0, 2)
|
||||
if err := db.GetEngine(ctx).Where(builder.Eq{"head_branch": headBranch, "head_repo_id": headRepoID}).
|
||||
Find(&prs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return prs, nil
|
||||
}
|
||||
|
||||
// GetBaseBranchHTMLURL returns the HTML URL of the base branch
|
||||
func (pr *PullRequest) GetBaseBranchHTMLURL() string {
|
||||
if err := pr.LoadBaseRepo(); err != nil {
|
||||
log.Error("LoadBaseRepo: %v", err)
|
||||
return ""
|
||||
}
|
||||
if pr.BaseRepo == nil {
|
||||
return ""
|
||||
}
|
||||
return pr.BaseRepo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(pr.BaseBranch)
|
||||
}
|
||||
|
||||
// GetHeadBranchHTMLURL returns the HTML URL of the head branch
|
||||
func (pr *PullRequest) GetHeadBranchHTMLURL() string {
|
||||
if pr.Flow == PullRequestFlowAGit {
|
||||
return ""
|
||||
}
|
||||
|
||||
if err := pr.LoadHeadRepo(); err != nil {
|
||||
log.Error("LoadHeadRepo: %v", err)
|
||||
return ""
|
||||
}
|
||||
if pr.HeadRepo == nil {
|
||||
return ""
|
||||
}
|
||||
return pr.HeadRepo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(pr.HeadBranch)
|
||||
}
|
||||
|
||||
// UpdateAllowEdits update if PR can be edited from maintainers
|
||||
func UpdateAllowEdits(ctx context.Context, pr *PullRequest) error {
|
||||
if _, err := db.GetEngine(ctx).ID(pr.ID).Cols("allow_maintainer_edit").Update(pr); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mergeable returns if the pullrequest is mergeable.
|
||||
func (pr *PullRequest) Mergeable() bool {
|
||||
// If a pull request isn't mergable if it's:
|
||||
// - Being conflict checked.
|
||||
// - Has a conflict.
|
||||
// - Received a error while being conflict checked.
|
||||
// - Is a work-in-progress pull request.
|
||||
return pr.Status != PullRequestStatusChecking && pr.Status != PullRequestStatusConflict &&
|
||||
pr.Status != PullRequestStatusError && !pr.IsWorkInProgress()
|
||||
}
|
||||
|
||||
// HasEnoughApprovals returns true if pr has enough granted approvals.
|
||||
func HasEnoughApprovals(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool {
|
||||
if protectBranch.RequiredApprovals == 0 {
|
||||
return true
|
||||
}
|
||||
return GetGrantedApprovalsCount(ctx, protectBranch, pr) >= protectBranch.RequiredApprovals
|
||||
}
|
||||
|
||||
// GetGrantedApprovalsCount returns the number of granted approvals for pr. A granted approval must be authored by a user in an approval whitelist.
|
||||
func GetGrantedApprovalsCount(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) int64 {
|
||||
sess := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID).
|
||||
And("type = ?", ReviewTypeApprove).
|
||||
And("official = ?", true).
|
||||
And("dismissed = ?", false)
|
||||
if protectBranch.DismissStaleApprovals {
|
||||
sess = sess.And("stale = ?", false)
|
||||
}
|
||||
approvals, err := sess.Count(new(Review))
|
||||
if err != nil {
|
||||
log.Error("GetGrantedApprovalsCount: %v", err)
|
||||
return 0
|
||||
}
|
||||
|
||||
return approvals
|
||||
}
|
||||
|
||||
// MergeBlockedByRejectedReview returns true if merge is blocked by rejected reviews
|
||||
func MergeBlockedByRejectedReview(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool {
|
||||
if !protectBranch.BlockOnRejectedReviews {
|
||||
return false
|
||||
}
|
||||
rejectExist, err := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID).
|
||||
And("type = ?", ReviewTypeReject).
|
||||
And("official = ?", true).
|
||||
And("dismissed = ?", false).
|
||||
Exist(new(Review))
|
||||
if err != nil {
|
||||
log.Error("MergeBlockedByRejectedReview: %v", err)
|
||||
return true
|
||||
}
|
||||
|
||||
return rejectExist
|
||||
}
|
||||
|
||||
// MergeBlockedByOfficialReviewRequests block merge because of some review request to official reviewer
|
||||
// of from official review
|
||||
func MergeBlockedByOfficialReviewRequests(ctx context.Context, protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool {
|
||||
if !protectBranch.BlockOnOfficialReviewRequests {
|
||||
return false
|
||||
}
|
||||
has, err := db.GetEngine(ctx).Where("issue_id = ?", pr.IssueID).
|
||||
And("type = ?", ReviewTypeRequest).
|
||||
And("official = ?", true).
|
||||
Exist(new(Review))
|
||||
if err != nil {
|
||||
log.Error("MergeBlockedByOfficialReviewRequests: %v", err)
|
||||
return true
|
||||
}
|
||||
|
||||
return has
|
||||
}
|
||||
|
||||
// MergeBlockedByOutdatedBranch returns true if merge is blocked by an outdated head branch
|
||||
func MergeBlockedByOutdatedBranch(protectBranch *git_model.ProtectedBranch, pr *PullRequest) bool {
|
||||
return protectBranch.BlockOnOutdatedBranch && pr.CommitsBehind > 0
|
||||
}
|
216
models/issues/pull_list.go
Normal file
216
models/issues/pull_list.go
Normal file
|
@ -0,0 +1,216 @@
|
|||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// PullRequestsOptions holds the options for PRs
|
||||
type PullRequestsOptions struct {
|
||||
db.ListOptions
|
||||
State string
|
||||
SortType string
|
||||
Labels []string
|
||||
MilestoneID int64
|
||||
}
|
||||
|
||||
func listPullRequestStatement(baseRepoID int64, opts *PullRequestsOptions) (*xorm.Session, error) {
|
||||
sess := db.GetEngine(db.DefaultContext).Where("pull_request.base_repo_id=?", baseRepoID)
|
||||
|
||||
sess.Join("INNER", "issue", "pull_request.issue_id = issue.id")
|
||||
switch opts.State {
|
||||
case "closed", "open":
|
||||
sess.And("issue.is_closed=?", opts.State == "closed")
|
||||
}
|
||||
|
||||
if labelIDs, err := base.StringsToInt64s(opts.Labels); err != nil {
|
||||
return nil, err
|
||||
} else if len(labelIDs) > 0 {
|
||||
sess.Join("INNER", "issue_label", "issue.id = issue_label.issue_id").
|
||||
In("issue_label.label_id", labelIDs)
|
||||
}
|
||||
|
||||
if opts.MilestoneID > 0 {
|
||||
sess.And("issue.milestone_id=?", opts.MilestoneID)
|
||||
}
|
||||
|
||||
return sess, nil
|
||||
}
|
||||
|
||||
// GetUnmergedPullRequestsByHeadInfo returns all pull requests that are open and has not been merged
|
||||
// by given head information (repo and branch).
|
||||
func GetUnmergedPullRequestsByHeadInfo(repoID int64, branch string) ([]*PullRequest, error) {
|
||||
prs := make([]*PullRequest, 0, 2)
|
||||
return prs, db.GetEngine(db.DefaultContext).
|
||||
Where("head_repo_id = ? AND head_branch = ? AND has_merged = ? AND issue.is_closed = ? AND flow = ?",
|
||||
repoID, branch, false, false, PullRequestFlowGithub).
|
||||
Join("INNER", "issue", "issue.id = pull_request.issue_id").
|
||||
Find(&prs)
|
||||
}
|
||||
|
||||
// CanMaintainerWriteToBranch check whether user is a matainer and could write to the branch
|
||||
func CanMaintainerWriteToBranch(p access_model.Permission, branch string, user *user_model.User) bool {
|
||||
if p.CanWrite(unit.TypeCode) {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(p.Units) < 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
prs, err := GetUnmergedPullRequestsByHeadInfo(p.Units[0].RepoID, branch)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, pr := range prs {
|
||||
if pr.AllowMaintainerEdit {
|
||||
err = pr.LoadBaseRepo()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
prPerm, err := access_model.GetUserRepoPermission(db.DefaultContext, pr.BaseRepo, user)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if prPerm.CanWrite(unit.TypeCode) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasUnmergedPullRequestsByHeadInfo checks if there are open and not merged pull request
|
||||
// by given head information (repo and branch)
|
||||
func HasUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch string) (bool, error) {
|
||||
return db.GetEngine(ctx).
|
||||
Where("head_repo_id = ? AND head_branch = ? AND has_merged = ? AND issue.is_closed = ? AND flow = ?",
|
||||
repoID, branch, false, false, PullRequestFlowGithub).
|
||||
Join("INNER", "issue", "issue.id = pull_request.issue_id").
|
||||
Exist(&PullRequest{})
|
||||
}
|
||||
|
||||
// GetUnmergedPullRequestsByBaseInfo returns all pull requests that are open and has not been merged
|
||||
// by given base information (repo and branch).
|
||||
func GetUnmergedPullRequestsByBaseInfo(repoID int64, branch string) ([]*PullRequest, error) {
|
||||
prs := make([]*PullRequest, 0, 2)
|
||||
return prs, db.GetEngine(db.DefaultContext).
|
||||
Where("base_repo_id=? AND base_branch=? AND has_merged=? AND issue.is_closed=?",
|
||||
repoID, branch, false, false).
|
||||
Join("INNER", "issue", "issue.id=pull_request.issue_id").
|
||||
Find(&prs)
|
||||
}
|
||||
|
||||
// GetPullRequestIDsByCheckStatus returns all pull requests according the special checking status.
|
||||
func GetPullRequestIDsByCheckStatus(status PullRequestStatus) ([]int64, error) {
|
||||
prs := make([]int64, 0, 10)
|
||||
return prs, db.GetEngine(db.DefaultContext).Table("pull_request").
|
||||
Where("status=?", status).
|
||||
Cols("pull_request.id").
|
||||
Find(&prs)
|
||||
}
|
||||
|
||||
// PullRequests returns all pull requests for a base Repo by the given conditions
|
||||
func PullRequests(baseRepoID int64, opts *PullRequestsOptions) ([]*PullRequest, int64, error) {
|
||||
if opts.Page <= 0 {
|
||||
opts.Page = 1
|
||||
}
|
||||
|
||||
countSession, err := listPullRequestStatement(baseRepoID, opts)
|
||||
if err != nil {
|
||||
log.Error("listPullRequestStatement: %v", err)
|
||||
return nil, 0, err
|
||||
}
|
||||
maxResults, err := countSession.Count(new(PullRequest))
|
||||
if err != nil {
|
||||
log.Error("Count PRs: %v", err)
|
||||
return nil, maxResults, err
|
||||
}
|
||||
|
||||
findSession, err := listPullRequestStatement(baseRepoID, opts)
|
||||
sortIssuesSession(findSession, opts.SortType, 0)
|
||||
if err != nil {
|
||||
log.Error("listPullRequestStatement: %v", err)
|
||||
return nil, maxResults, err
|
||||
}
|
||||
findSession = db.SetSessionPagination(findSession, opts)
|
||||
prs := make([]*PullRequest, 0, opts.PageSize)
|
||||
return prs, maxResults, findSession.Find(&prs)
|
||||
}
|
||||
|
||||
// PullRequestList defines a list of pull requests
|
||||
type PullRequestList []*PullRequest
|
||||
|
||||
func (prs PullRequestList) loadAttributes(ctx context.Context) error {
|
||||
if len(prs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load issues.
|
||||
issueIDs := prs.getIssueIDs()
|
||||
issues := make([]*Issue, 0, len(issueIDs))
|
||||
if err := db.GetEngine(ctx).
|
||||
Where("id > 0").
|
||||
In("id", issueIDs).
|
||||
Find(&issues); err != nil {
|
||||
return fmt.Errorf("find issues: %v", err)
|
||||
}
|
||||
|
||||
set := make(map[int64]*Issue)
|
||||
for i := range issues {
|
||||
set[issues[i].ID] = issues[i]
|
||||
}
|
||||
for i := range prs {
|
||||
prs[i].Issue = set[prs[i].IssueID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (prs PullRequestList) getIssueIDs() []int64 {
|
||||
issueIDs := make([]int64, 0, len(prs))
|
||||
for i := range prs {
|
||||
issueIDs = append(issueIDs, prs[i].IssueID)
|
||||
}
|
||||
return issueIDs
|
||||
}
|
||||
|
||||
// LoadAttributes load all the prs attributes
|
||||
func (prs PullRequestList) LoadAttributes() error {
|
||||
return prs.loadAttributes(db.DefaultContext)
|
||||
}
|
||||
|
||||
// InvalidateCodeComments will lookup the prs for code comments which got invalidated by change
|
||||
func (prs PullRequestList) InvalidateCodeComments(ctx context.Context, doer *user_model.User, repo *git.Repository, branch string) error {
|
||||
if len(prs) == 0 {
|
||||
return nil
|
||||
}
|
||||
issueIDs := prs.getIssueIDs()
|
||||
var codeComments []*Comment
|
||||
if err := db.GetEngine(ctx).
|
||||
Where("type = ? and invalidated = ?", CommentTypeCode, false).
|
||||
In("issue_id", issueIDs).
|
||||
Find(&codeComments); err != nil {
|
||||
return fmt.Errorf("find code comments: %v", err)
|
||||
}
|
||||
for _, comment := range codeComments {
|
||||
if err := comment.CheckInvalidation(repo, doer, branch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
277
models/issues/pull_test.go
Normal file
277
models/issues/pull_test.go
Normal file
|
@ -0,0 +1,277 @@
|
|||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPullRequest_LoadAttributes(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest)
|
||||
assert.NoError(t, pr.LoadAttributes())
|
||||
assert.NotNil(t, pr.Merger)
|
||||
assert.Equal(t, pr.MergerID, pr.Merger.ID)
|
||||
}
|
||||
|
||||
func TestPullRequest_LoadIssue(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest)
|
||||
assert.NoError(t, pr.LoadIssue())
|
||||
assert.NotNil(t, pr.Issue)
|
||||
assert.Equal(t, int64(2), pr.Issue.ID)
|
||||
assert.NoError(t, pr.LoadIssue())
|
||||
assert.NotNil(t, pr.Issue)
|
||||
assert.Equal(t, int64(2), pr.Issue.ID)
|
||||
}
|
||||
|
||||
func TestPullRequest_LoadBaseRepo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest)
|
||||
assert.NoError(t, pr.LoadBaseRepo())
|
||||
assert.NotNil(t, pr.BaseRepo)
|
||||
assert.Equal(t, pr.BaseRepoID, pr.BaseRepo.ID)
|
||||
assert.NoError(t, pr.LoadBaseRepo())
|
||||
assert.NotNil(t, pr.BaseRepo)
|
||||
assert.Equal(t, pr.BaseRepoID, pr.BaseRepo.ID)
|
||||
}
|
||||
|
||||
func TestPullRequest_LoadHeadRepo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest)
|
||||
assert.NoError(t, pr.LoadHeadRepo())
|
||||
assert.NotNil(t, pr.HeadRepo)
|
||||
assert.Equal(t, pr.HeadRepoID, pr.HeadRepo.ID)
|
||||
}
|
||||
|
||||
// TODO TestMerge
|
||||
|
||||
// TODO TestNewPullRequest
|
||||
|
||||
func TestPullRequestsNewest(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
prs, count, err := issues_model.PullRequests(1, &issues_model.PullRequestsOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
},
|
||||
State: "open",
|
||||
SortType: "newest",
|
||||
Labels: []string{},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 3, count)
|
||||
if assert.Len(t, prs, 3) {
|
||||
assert.EqualValues(t, 5, prs[0].ID)
|
||||
assert.EqualValues(t, 2, prs[1].ID)
|
||||
assert.EqualValues(t, 1, prs[2].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullRequestsOldest(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
prs, count, err := issues_model.PullRequests(1, &issues_model.PullRequestsOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
},
|
||||
State: "open",
|
||||
SortType: "oldest",
|
||||
Labels: []string{},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 3, count)
|
||||
if assert.Len(t, prs, 3) {
|
||||
assert.EqualValues(t, 1, prs[0].ID)
|
||||
assert.EqualValues(t, 2, prs[1].ID)
|
||||
assert.EqualValues(t, 5, prs[2].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUnmergedPullRequest(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
pr, err := issues_model.GetUnmergedPullRequest(1, 1, "branch2", "master", issues_model.PullRequestFlowGithub)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), pr.ID)
|
||||
|
||||
_, err = issues_model.GetUnmergedPullRequest(1, 9223372036854775807, "branch1", "master", issues_model.PullRequestFlowGithub)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
||||
}
|
||||
|
||||
func TestHasUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(db.DefaultContext, 1, "branch2")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, exist)
|
||||
|
||||
exist, err = issues_model.HasUnmergedPullRequestsByHeadInfo(db.DefaultContext, 1, "not_exist_branch")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, false, exist)
|
||||
}
|
||||
|
||||
func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
prs, err := issues_model.GetUnmergedPullRequestsByHeadInfo(1, "branch2")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, prs, 1)
|
||||
for _, pr := range prs {
|
||||
assert.Equal(t, int64(1), pr.HeadRepoID)
|
||||
assert.Equal(t, "branch2", pr.HeadBranch)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(1, "master")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, prs, 1)
|
||||
pr := prs[0]
|
||||
assert.Equal(t, int64(2), pr.ID)
|
||||
assert.Equal(t, int64(1), pr.BaseRepoID)
|
||||
assert.Equal(t, "master", pr.BaseBranch)
|
||||
}
|
||||
|
||||
func TestGetPullRequestByIndex(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
pr, err := issues_model.GetPullRequestByIndex(db.DefaultContext, 1, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), pr.BaseRepoID)
|
||||
assert.Equal(t, int64(2), pr.Index)
|
||||
|
||||
_, err = issues_model.GetPullRequestByIndex(db.DefaultContext, 9223372036854775807, 9223372036854775807)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
||||
|
||||
_, err = issues_model.GetPullRequestByIndex(db.DefaultContext, 1, 0)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetPullRequestByID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
pr, err := issues_model.GetPullRequestByID(db.DefaultContext, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), pr.ID)
|
||||
assert.Equal(t, int64(2), pr.IssueID)
|
||||
|
||||
_, err = issues_model.GetPullRequestByID(db.DefaultContext, 9223372036854775807)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
||||
}
|
||||
|
||||
func TestGetPullRequestByIssueID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
pr, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, 2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), pr.IssueID)
|
||||
|
||||
_, err = issues_model.GetPullRequestByIssueID(db.DefaultContext, 9223372036854775807)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, issues_model.IsErrPullRequestNotExist(err))
|
||||
}
|
||||
|
||||
func TestPullRequest_Update(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest)
|
||||
pr.BaseBranch = "baseBranch"
|
||||
pr.HeadBranch = "headBranch"
|
||||
pr.Update()
|
||||
|
||||
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: pr.ID}).(*issues_model.PullRequest)
|
||||
assert.Equal(t, "baseBranch", pr.BaseBranch)
|
||||
assert.Equal(t, "headBranch", pr.HeadBranch)
|
||||
unittest.CheckConsistencyFor(t, pr)
|
||||
}
|
||||
|
||||
func TestPullRequest_UpdateCols(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
pr := &issues_model.PullRequest{
|
||||
ID: 1,
|
||||
BaseBranch: "baseBranch",
|
||||
HeadBranch: "headBranch",
|
||||
}
|
||||
assert.NoError(t, pr.UpdateCols("head_branch"))
|
||||
|
||||
pr = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest)
|
||||
assert.Equal(t, "master", pr.BaseBranch)
|
||||
assert.Equal(t, "headBranch", pr.HeadBranch)
|
||||
unittest.CheckConsistencyFor(t, pr)
|
||||
}
|
||||
|
||||
func TestPullRequestList_LoadAttributes(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
prs := []*issues_model.PullRequest{
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}).(*issues_model.PullRequest),
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest),
|
||||
}
|
||||
assert.NoError(t, issues_model.PullRequestList(prs).LoadAttributes())
|
||||
for _, pr := range prs {
|
||||
assert.NotNil(t, pr.Issue)
|
||||
assert.Equal(t, pr.IssueID, pr.Issue.ID)
|
||||
}
|
||||
|
||||
assert.NoError(t, issues_model.PullRequestList([]*issues_model.PullRequest{}).LoadAttributes())
|
||||
}
|
||||
|
||||
// TODO TestAddTestPullRequestTask
|
||||
|
||||
func TestPullRequest_IsWorkInProgress(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest)
|
||||
pr.LoadIssue()
|
||||
|
||||
assert.False(t, pr.IsWorkInProgress())
|
||||
|
||||
pr.Issue.Title = "WIP: " + pr.Issue.Title
|
||||
assert.True(t, pr.IsWorkInProgress())
|
||||
|
||||
pr.Issue.Title = "[wip]: " + pr.Issue.Title
|
||||
assert.True(t, pr.IsWorkInProgress())
|
||||
}
|
||||
|
||||
func TestPullRequest_GetWorkInProgressPrefixWorkInProgress(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 2}).(*issues_model.PullRequest)
|
||||
pr.LoadIssue()
|
||||
|
||||
assert.Empty(t, pr.GetWorkInProgressPrefix())
|
||||
|
||||
original := pr.Issue.Title
|
||||
pr.Issue.Title = "WIP: " + original
|
||||
assert.Equal(t, "WIP:", pr.GetWorkInProgressPrefix())
|
||||
|
||||
pr.Issue.Title = "[wip] " + original
|
||||
assert.Equal(t, "[wip]", pr.GetWorkInProgressPrefix())
|
||||
}
|
||||
|
||||
func TestDeleteOrphanedObjects(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
countBefore, err := db.GetEngine(db.DefaultContext).Count(&issues_model.PullRequest{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = db.GetEngine(db.DefaultContext).Insert(&issues_model.PullRequest{IssueID: 1000}, &issues_model.PullRequest{IssueID: 1001}, &issues_model.PullRequest{IssueID: 1003})
|
||||
assert.NoError(t, err)
|
||||
|
||||
orphaned, err := db.CountOrphanedObjects("pull_request", "issue", "pull_request.issue_id=issue.id")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 3, orphaned)
|
||||
|
||||
err = db.DeleteOrphanedObjects("pull_request", "issue", "pull_request.issue_id=issue.id")
|
||||
assert.NoError(t, err)
|
||||
|
||||
countAfter, err := db.GetEngine(db.DefaultContext).Count(&issues_model.PullRequest{})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, countBefore, countAfter)
|
||||
}
|
|
@ -2,12 +2,13 @@
|
|||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues
|
||||
package issues_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
@ -17,12 +18,12 @@ import (
|
|||
)
|
||||
|
||||
func addReaction(t *testing.T, doerID, issueID, commentID int64, content string) {
|
||||
var reaction *Reaction
|
||||
var reaction *issues_model.Reaction
|
||||
var err error
|
||||
if commentID == 0 {
|
||||
reaction, err = CreateIssueReaction(doerID, issueID, content)
|
||||
reaction, err = issues_model.CreateIssueReaction(doerID, issueID, content)
|
||||
} else {
|
||||
reaction, err = CreateCommentReaction(doerID, issueID, commentID, content)
|
||||
reaction, err = issues_model.CreateCommentReaction(doerID, issueID, commentID, content)
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, reaction)
|
||||
|
@ -37,7 +38,7 @@ func TestIssueAddReaction(t *testing.T) {
|
|||
|
||||
addReaction(t, user1.ID, issue1ID, 0, "heart")
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
|
||||
}
|
||||
|
||||
func TestIssueAddDuplicateReaction(t *testing.T) {
|
||||
|
@ -49,15 +50,15 @@ func TestIssueAddDuplicateReaction(t *testing.T) {
|
|||
|
||||
addReaction(t, user1.ID, issue1ID, 0, "heart")
|
||||
|
||||
reaction, err := CreateReaction(&ReactionOptions{
|
||||
reaction, err := issues_model.CreateReaction(&issues_model.ReactionOptions{
|
||||
DoerID: user1.ID,
|
||||
IssueID: issue1ID,
|
||||
Type: "heart",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, ErrReactionAlreadyExist{Reaction: "heart"}, err)
|
||||
assert.Equal(t, issues_model.ErrReactionAlreadyExist{Reaction: "heart"}, err)
|
||||
|
||||
existingR := unittest.AssertExistsAndLoadBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID}).(*Reaction)
|
||||
existingR := unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID}).(*issues_model.Reaction)
|
||||
assert.Equal(t, existingR.ID, reaction.ID)
|
||||
}
|
||||
|
||||
|
@ -70,10 +71,10 @@ func TestIssueDeleteReaction(t *testing.T) {
|
|||
|
||||
addReaction(t, user1.ID, issue1ID, 0, "heart")
|
||||
|
||||
err := DeleteIssueReaction(user1.ID, issue1ID, "heart")
|
||||
err := issues_model.DeleteIssueReaction(user1.ID, issue1ID, "heart")
|
||||
assert.NoError(t, err)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
|
||||
unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID})
|
||||
}
|
||||
|
||||
func TestIssueReactionCount(t *testing.T) {
|
||||
|
@ -98,7 +99,7 @@ func TestIssueReactionCount(t *testing.T) {
|
|||
addReaction(t, user4.ID, issueID, 0, "heart")
|
||||
addReaction(t, ghost.ID, issueID, 0, "-1")
|
||||
|
||||
reactionsList, _, err := FindReactions(db.DefaultContext, FindReactionsOptions{
|
||||
reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
|
||||
IssueID: issueID,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
@ -128,7 +129,7 @@ func TestIssueCommentAddReaction(t *testing.T) {
|
|||
|
||||
addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
|
||||
}
|
||||
|
||||
func TestIssueCommentDeleteReaction(t *testing.T) {
|
||||
|
@ -147,7 +148,7 @@ func TestIssueCommentDeleteReaction(t *testing.T) {
|
|||
addReaction(t, user3.ID, issue1ID, comment1ID, "heart")
|
||||
addReaction(t, user4.ID, issue1ID, comment1ID, "+1")
|
||||
|
||||
reactionsList, _, err := FindReactions(db.DefaultContext, FindReactionsOptions{
|
||||
reactionsList, _, err := issues_model.FindReactions(db.DefaultContext, issues_model.FindReactionsOptions{
|
||||
IssueID: issue1ID,
|
||||
CommentID: comment1ID,
|
||||
})
|
||||
|
@ -168,7 +169,7 @@ func TestIssueCommentReactionCount(t *testing.T) {
|
|||
var comment1ID int64 = 1
|
||||
|
||||
addReaction(t, user1.ID, issue1ID, comment1ID, "heart")
|
||||
assert.NoError(t, DeleteCommentReaction(user1.ID, issue1ID, comment1ID, "heart"))
|
||||
assert.NoError(t, issues_model.DeleteCommentReaction(user1.ID, issue1ID, comment1ID, "heart"))
|
||||
|
||||
unittest.AssertNotExistsBean(t, &Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
|
||||
unittest.AssertNotExistsBean(t, &issues_model.Reaction{Type: "heart", UserID: user1.ID, IssueID: issue1ID, CommentID: comment1ID})
|
||||
}
|
||||
|
|
1018
models/issues/review.go
Normal file
1018
models/issues/review.go
Normal file
File diff suppressed because it is too large
Load diff
203
models/issues/review_test.go
Normal file
203
models/issues/review_test.go
Normal file
|
@ -0,0 +1,203 @@
|
|||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetReviewByID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
review, err := issues_model.GetReviewByID(db.DefaultContext, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Demo Review", review.Content)
|
||||
assert.Equal(t, issues_model.ReviewTypeApprove, review.Type)
|
||||
|
||||
_, err = issues_model.GetReviewByID(db.DefaultContext, 23892)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, issues_model.IsErrReviewNotExist(err), "IsErrReviewNotExist")
|
||||
}
|
||||
|
||||
func TestReview_LoadAttributes(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 1}).(*issues_model.Review)
|
||||
assert.NoError(t, review.LoadAttributes(db.DefaultContext))
|
||||
assert.NotNil(t, review.Issue)
|
||||
assert.NotNil(t, review.Reviewer)
|
||||
|
||||
invalidReview1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 2}).(*issues_model.Review)
|
||||
assert.Error(t, invalidReview1.LoadAttributes(db.DefaultContext))
|
||||
|
||||
invalidReview2 := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 3}).(*issues_model.Review)
|
||||
assert.Error(t, invalidReview2.LoadAttributes(db.DefaultContext))
|
||||
}
|
||||
|
||||
func TestReview_LoadCodeComments(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
review := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 4}).(*issues_model.Review)
|
||||
assert.NoError(t, review.LoadAttributes(db.DefaultContext))
|
||||
assert.NoError(t, review.LoadCodeComments(db.DefaultContext))
|
||||
assert.Len(t, review.CodeComments, 1)
|
||||
assert.Equal(t, int64(4), review.CodeComments["README.md"][int64(4)][0].Line)
|
||||
}
|
||||
|
||||
func TestReviewType_Icon(t *testing.T) {
|
||||
assert.Equal(t, "check", issues_model.ReviewTypeApprove.Icon())
|
||||
assert.Equal(t, "diff", issues_model.ReviewTypeReject.Icon())
|
||||
assert.Equal(t, "comment", issues_model.ReviewTypeComment.Icon())
|
||||
assert.Equal(t, "comment", issues_model.ReviewTypeUnknown.Icon())
|
||||
assert.Equal(t, "dot-fill", issues_model.ReviewTypeRequest.Icon())
|
||||
assert.Equal(t, "comment", issues_model.ReviewType(6).Icon())
|
||||
}
|
||||
|
||||
func TestFindReviews(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
reviews, err := issues_model.FindReviews(db.DefaultContext, issues_model.FindReviewOptions{
|
||||
Type: issues_model.ReviewTypeApprove,
|
||||
IssueID: 2,
|
||||
ReviewerID: 1,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, reviews, 1)
|
||||
assert.Equal(t, "Demo Review", reviews[0].Content)
|
||||
}
|
||||
|
||||
func TestGetCurrentReview(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue)
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
|
||||
|
||||
review, err := issues_model.GetCurrentReview(db.DefaultContext, user, issue)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, review)
|
||||
assert.Equal(t, issues_model.ReviewTypePending, review.Type)
|
||||
assert.Equal(t, "Pending Review", review.Content)
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 7}).(*user_model.User)
|
||||
review2, err := issues_model.GetCurrentReview(db.DefaultContext, user2, issue)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, issues_model.IsErrReviewNotExist(err))
|
||||
assert.Nil(t, review2)
|
||||
}
|
||||
|
||||
func TestCreateReview(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2}).(*issues_model.Issue)
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
|
||||
|
||||
review, err := issues_model.CreateReview(db.DefaultContext, issues_model.CreateReviewOptions{
|
||||
Content: "New Review",
|
||||
Type: issues_model.ReviewTypePending,
|
||||
Issue: issue,
|
||||
Reviewer: user,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "New Review", review.Content)
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.Review{Content: "New Review"})
|
||||
}
|
||||
|
||||
func TestGetReviewersByIssueID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 3}).(*issues_model.Issue)
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
|
||||
user3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}).(*user_model.User)
|
||||
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}).(*user_model.User)
|
||||
|
||||
expectedReviews := []*issues_model.Review{}
|
||||
expectedReviews = append(expectedReviews,
|
||||
&issues_model.Review{
|
||||
Reviewer: user3,
|
||||
Type: issues_model.ReviewTypeReject,
|
||||
UpdatedUnix: 946684812,
|
||||
},
|
||||
&issues_model.Review{
|
||||
Reviewer: user4,
|
||||
Type: issues_model.ReviewTypeApprove,
|
||||
UpdatedUnix: 946684813,
|
||||
},
|
||||
&issues_model.Review{
|
||||
Reviewer: user2,
|
||||
Type: issues_model.ReviewTypeReject,
|
||||
UpdatedUnix: 946684814,
|
||||
})
|
||||
|
||||
allReviews, err := issues_model.GetReviewersByIssueID(issue.ID)
|
||||
for _, reviewer := range allReviews {
|
||||
assert.NoError(t, reviewer.LoadReviewer())
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, allReviews, 3) {
|
||||
for i, review := range allReviews {
|
||||
assert.Equal(t, expectedReviews[i].Reviewer, review.Reviewer)
|
||||
assert.Equal(t, expectedReviews[i].Type, review.Type)
|
||||
assert.Equal(t, expectedReviews[i].UpdatedUnix, review.UpdatedUnix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDismissReview(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
rejectReviewExample := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}).(*issues_model.Review)
|
||||
requestReviewExample := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}).(*issues_model.Review)
|
||||
approveReviewExample := unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 8}).(*issues_model.Review)
|
||||
assert.False(t, rejectReviewExample.Dismissed)
|
||||
assert.False(t, requestReviewExample.Dismissed)
|
||||
assert.False(t, approveReviewExample.Dismissed)
|
||||
|
||||
assert.NoError(t, issues_model.DismissReview(rejectReviewExample, true))
|
||||
rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}).(*issues_model.Review)
|
||||
requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}).(*issues_model.Review)
|
||||
assert.True(t, rejectReviewExample.Dismissed)
|
||||
assert.False(t, requestReviewExample.Dismissed)
|
||||
|
||||
assert.NoError(t, issues_model.DismissReview(requestReviewExample, true))
|
||||
rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}).(*issues_model.Review)
|
||||
requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}).(*issues_model.Review)
|
||||
assert.True(t, rejectReviewExample.Dismissed)
|
||||
assert.False(t, requestReviewExample.Dismissed)
|
||||
assert.False(t, approveReviewExample.Dismissed)
|
||||
|
||||
assert.NoError(t, issues_model.DismissReview(requestReviewExample, true))
|
||||
rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}).(*issues_model.Review)
|
||||
requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}).(*issues_model.Review)
|
||||
assert.True(t, rejectReviewExample.Dismissed)
|
||||
assert.False(t, requestReviewExample.Dismissed)
|
||||
assert.False(t, approveReviewExample.Dismissed)
|
||||
|
||||
assert.NoError(t, issues_model.DismissReview(requestReviewExample, false))
|
||||
rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}).(*issues_model.Review)
|
||||
requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}).(*issues_model.Review)
|
||||
assert.True(t, rejectReviewExample.Dismissed)
|
||||
assert.False(t, requestReviewExample.Dismissed)
|
||||
assert.False(t, approveReviewExample.Dismissed)
|
||||
|
||||
assert.NoError(t, issues_model.DismissReview(requestReviewExample, false))
|
||||
rejectReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 9}).(*issues_model.Review)
|
||||
requestReviewExample = unittest.AssertExistsAndLoadBean(t, &issues_model.Review{ID: 11}).(*issues_model.Review)
|
||||
assert.True(t, rejectReviewExample.Dismissed)
|
||||
assert.False(t, requestReviewExample.Dismissed)
|
||||
assert.False(t, approveReviewExample.Dismissed)
|
||||
|
||||
assert.NoError(t, issues_model.DismissReview(rejectReviewExample, false))
|
||||
assert.False(t, rejectReviewExample.Dismissed)
|
||||
assert.False(t, requestReviewExample.Dismissed)
|
||||
assert.False(t, approveReviewExample.Dismissed)
|
||||
|
||||
assert.NoError(t, issues_model.DismissReview(approveReviewExample, true))
|
||||
assert.False(t, rejectReviewExample.Dismissed)
|
||||
assert.False(t, requestReviewExample.Dismissed)
|
||||
assert.True(t, approveReviewExample.Dismissed)
|
||||
}
|
293
models/issues/stopwatch.go
Normal file
293
models/issues/stopwatch.go
Normal file
|
@ -0,0 +1,293 @@
|
|||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// ErrIssueStopwatchNotExist represents an error that stopwatch is not exist
|
||||
type ErrIssueStopwatchNotExist struct {
|
||||
UserID int64
|
||||
IssueID int64
|
||||
}
|
||||
|
||||
func (err ErrIssueStopwatchNotExist) Error() string {
|
||||
return fmt.Sprintf("issue stopwatch doesn't exist[uid: %d, issue_id: %d", err.UserID, err.IssueID)
|
||||
}
|
||||
|
||||
// ErrIssueStopwatchAlreadyExist represents an error that stopwatch is already exist
|
||||
type ErrIssueStopwatchAlreadyExist struct {
|
||||
UserID int64
|
||||
IssueID int64
|
||||
}
|
||||
|
||||
func (err ErrIssueStopwatchAlreadyExist) Error() string {
|
||||
return fmt.Sprintf("issue stopwatch already exists[uid: %d, issue_id: %d", err.UserID, err.IssueID)
|
||||
}
|
||||
|
||||
// Stopwatch represents a stopwatch for time tracking.
|
||||
type Stopwatch struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
UserID int64 `xorm:"INDEX"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(Stopwatch))
|
||||
}
|
||||
|
||||
// Seconds returns the amount of time passed since creation, based on local server time
|
||||
func (s Stopwatch) Seconds() int64 {
|
||||
return int64(timeutil.TimeStampNow() - s.CreatedUnix)
|
||||
}
|
||||
|
||||
// Duration returns a human-readable duration string based on local server time
|
||||
func (s Stopwatch) Duration() string {
|
||||
return util.SecToTime(s.Seconds())
|
||||
}
|
||||
|
||||
func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) {
|
||||
sw = new(Stopwatch)
|
||||
exists, err = db.GetEngine(ctx).
|
||||
Where("user_id = ?", userID).
|
||||
And("issue_id = ?", issueID).
|
||||
Get(sw)
|
||||
return
|
||||
}
|
||||
|
||||
// UserIDCount is a simple coalition of UserID and Count
|
||||
type UserStopwatch struct {
|
||||
UserID int64
|
||||
StopWatches []*Stopwatch
|
||||
}
|
||||
|
||||
// GetUIDsAndNotificationCounts between the two provided times
|
||||
func GetUIDsAndStopwatch() ([]*UserStopwatch, error) {
|
||||
sws := []*Stopwatch{}
|
||||
if err := db.GetEngine(db.DefaultContext).Where("issue_id != 0").Find(&sws); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(sws) == 0 {
|
||||
return []*UserStopwatch{}, nil
|
||||
}
|
||||
|
||||
lastUserID := int64(-1)
|
||||
res := []*UserStopwatch{}
|
||||
for _, sw := range sws {
|
||||
if lastUserID == sw.UserID {
|
||||
lastUserStopwatch := res[len(res)-1]
|
||||
lastUserStopwatch.StopWatches = append(lastUserStopwatch.StopWatches, sw)
|
||||
} else {
|
||||
res = append(res, &UserStopwatch{
|
||||
UserID: sw.UserID,
|
||||
StopWatches: []*Stopwatch{sw},
|
||||
})
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GetUserStopwatches return list of all stopwatches of a user
|
||||
func GetUserStopwatches(userID int64, listOptions db.ListOptions) ([]*Stopwatch, error) {
|
||||
sws := make([]*Stopwatch, 0, 8)
|
||||
sess := db.GetEngine(db.DefaultContext).Where("stopwatch.user_id = ?", userID)
|
||||
if listOptions.Page != 0 {
|
||||
sess = db.SetSessionPagination(sess, &listOptions)
|
||||
}
|
||||
|
||||
err := sess.Find(&sws)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sws, nil
|
||||
}
|
||||
|
||||
// CountUserStopwatches return count of all stopwatches of a user
|
||||
func CountUserStopwatches(userID int64) (int64, error) {
|
||||
return db.GetEngine(db.DefaultContext).Where("user_id = ?", userID).Count(&Stopwatch{})
|
||||
}
|
||||
|
||||
// StopwatchExists returns true if the stopwatch exists
|
||||
func StopwatchExists(userID, issueID int64) bool {
|
||||
_, exists, _ := getStopwatch(db.DefaultContext, userID, issueID)
|
||||
return exists
|
||||
}
|
||||
|
||||
// HasUserStopwatch returns true if the user has a stopwatch
|
||||
func HasUserStopwatch(ctx context.Context, userID int64) (exists bool, sw *Stopwatch, err error) {
|
||||
sw = new(Stopwatch)
|
||||
exists, err = db.GetEngine(ctx).
|
||||
Where("user_id = ?", userID).
|
||||
Get(sw)
|
||||
return
|
||||
}
|
||||
|
||||
// FinishIssueStopwatchIfPossible if stopwatch exist then finish it otherwise ignore
|
||||
func FinishIssueStopwatchIfPossible(ctx context.Context, user *user_model.User, issue *Issue) error {
|
||||
_, exists, err := getStopwatch(ctx, user.ID, issue.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
return FinishIssueStopwatch(ctx, user, issue)
|
||||
}
|
||||
|
||||
// CreateOrStopIssueStopwatch create an issue stopwatch if it's not exist, otherwise finish it
|
||||
func CreateOrStopIssueStopwatch(user *user_model.User, issue *Issue) error {
|
||||
_, exists, err := getStopwatch(db.DefaultContext, user.ID, issue.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return FinishIssueStopwatch(db.DefaultContext, user, issue)
|
||||
}
|
||||
return CreateIssueStopwatch(db.DefaultContext, user, issue)
|
||||
}
|
||||
|
||||
// FinishIssueStopwatch if stopwatch exist then finish it otherwise return an error
|
||||
func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error {
|
||||
sw, exists, err := getStopwatch(ctx, user.ID, issue.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return ErrIssueStopwatchNotExist{
|
||||
UserID: user.ID,
|
||||
IssueID: issue.ID,
|
||||
}
|
||||
}
|
||||
|
||||
// Create tracked time out of the time difference between start date and actual date
|
||||
timediff := time.Now().Unix() - int64(sw.CreatedUnix)
|
||||
|
||||
// Create TrackedTime
|
||||
tt := &TrackedTime{
|
||||
Created: time.Now(),
|
||||
IssueID: issue.ID,
|
||||
UserID: user.ID,
|
||||
Time: timediff,
|
||||
}
|
||||
|
||||
if err := db.Insert(ctx, tt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{
|
||||
Doer: user,
|
||||
Issue: issue,
|
||||
Repo: issue.Repo,
|
||||
Content: util.SecToTime(timediff),
|
||||
Type: CommentTypeStopTracking,
|
||||
TimeID: tt.ID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.DeleteByBean(ctx, sw)
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateIssueStopwatch creates a stopwatch if not exist, otherwise return an error
|
||||
func CreateIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error {
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if another stopwatch is running: stop it
|
||||
exists, sw, err := HasUserStopwatch(ctx, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
issue, err := GetIssueByID(ctx, sw.IssueID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := FinishIssueStopwatch(ctx, user, issue); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create stopwatch
|
||||
sw = &Stopwatch{
|
||||
UserID: user.ID,
|
||||
IssueID: issue.ID,
|
||||
}
|
||||
|
||||
if err := db.Insert(ctx, sw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{
|
||||
Doer: user,
|
||||
Issue: issue,
|
||||
Repo: issue.Repo,
|
||||
Type: CommentTypeStartTracking,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelStopwatch removes the given stopwatch and logs it into issue's timeline.
|
||||
func CancelStopwatch(user *user_model.User, issue *Issue) error {
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
if err := cancelStopwatch(ctx, user, issue); err != nil {
|
||||
return err
|
||||
}
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
func cancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) error {
|
||||
e := db.GetEngine(ctx)
|
||||
sw, exists, err := getStopwatch(ctx, user.ID, issue.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exists {
|
||||
if _, err := e.Delete(sw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{
|
||||
Doer: user,
|
||||
Issue: issue,
|
||||
Repo: issue.Repo,
|
||||
Type: CommentTypeCancelTracking,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
79
models/issues/stopwatch_test.go
Normal file
79
models/issues/stopwatch_test.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCancelStopwatch(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user1, err := user_model.GetUserByID(1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1)
|
||||
assert.NoError(t, err)
|
||||
issue2, err := issues_model.GetIssueByID(db.DefaultContext, 2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = issues_model.CancelStopwatch(user1, issue1)
|
||||
assert.NoError(t, err)
|
||||
unittest.AssertNotExistsBean(t, &issues_model.Stopwatch{UserID: user1.ID, IssueID: issue1.ID})
|
||||
|
||||
_ = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeCancelTracking, PosterID: user1.ID, IssueID: issue1.ID})
|
||||
|
||||
assert.Nil(t, issues_model.CancelStopwatch(user1, issue2))
|
||||
}
|
||||
|
||||
func TestStopwatchExists(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
assert.True(t, issues_model.StopwatchExists(1, 1))
|
||||
assert.False(t, issues_model.StopwatchExists(1, 2))
|
||||
}
|
||||
|
||||
func TestHasUserStopwatch(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
exists, sw, err := issues_model.HasUserStopwatch(db.DefaultContext, 1)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, int64(1), sw.ID)
|
||||
|
||||
exists, _, err = issues_model.HasUserStopwatch(db.DefaultContext, 3)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exists)
|
||||
}
|
||||
|
||||
func TestCreateOrStopIssueStopwatch(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user2, err := user_model.GetUserByID(2)
|
||||
assert.NoError(t, err)
|
||||
user3, err := user_model.GetUserByID(3)
|
||||
assert.NoError(t, err)
|
||||
|
||||
issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1)
|
||||
assert.NoError(t, err)
|
||||
issue2, err := issues_model.GetIssueByID(db.DefaultContext, 2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, issues_model.CreateOrStopIssueStopwatch(user3, issue1))
|
||||
sw := unittest.AssertExistsAndLoadBean(t, &issues_model.Stopwatch{UserID: 3, IssueID: 1}).(*issues_model.Stopwatch)
|
||||
assert.LessOrEqual(t, sw.CreatedUnix, timeutil.TimeStampNow())
|
||||
|
||||
assert.NoError(t, issues_model.CreateOrStopIssueStopwatch(user2, issue2))
|
||||
unittest.AssertNotExistsBean(t, &issues_model.Stopwatch{UserID: 2, IssueID: 2})
|
||||
unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{UserID: 2, IssueID: 2})
|
||||
}
|
316
models/issues/tracked_time.go
Normal file
316
models/issues/tracked_time.go
Normal file
|
@ -0,0 +1,316 @@
|
|||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// TrackedTime represents a time that was spent for a specific issue.
|
||||
type TrackedTime struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
Issue *Issue `xorm:"-"`
|
||||
UserID int64 `xorm:"INDEX"`
|
||||
User *user_model.User `xorm:"-"`
|
||||
Created time.Time `xorm:"-"`
|
||||
CreatedUnix int64 `xorm:"created"`
|
||||
Time int64 `xorm:"NOT NULL"`
|
||||
Deleted bool `xorm:"NOT NULL DEFAULT false"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(TrackedTime))
|
||||
}
|
||||
|
||||
// TrackedTimeList is a List of TrackedTime's
|
||||
type TrackedTimeList []*TrackedTime
|
||||
|
||||
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
|
||||
func (t *TrackedTime) AfterLoad() {
|
||||
t.Created = time.Unix(t.CreatedUnix, 0).In(setting.DefaultUILocation)
|
||||
}
|
||||
|
||||
// LoadAttributes load Issue, User
|
||||
func (t *TrackedTime) LoadAttributes() (err error) {
|
||||
return t.loadAttributes(db.DefaultContext)
|
||||
}
|
||||
|
||||
func (t *TrackedTime) loadAttributes(ctx context.Context) (err error) {
|
||||
if t.Issue == nil {
|
||||
t.Issue, err = GetIssueByID(ctx, t.IssueID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = t.Issue.LoadRepo(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if t.User == nil {
|
||||
t.User, err = user_model.GetUserByIDCtx(ctx, t.UserID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// LoadAttributes load Issue, User
|
||||
func (tl TrackedTimeList) LoadAttributes() (err error) {
|
||||
for _, t := range tl {
|
||||
if err = t.LoadAttributes(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// FindTrackedTimesOptions represent the filters for tracked times. If an ID is 0 it will be ignored.
|
||||
type FindTrackedTimesOptions struct {
|
||||
db.ListOptions
|
||||
IssueID int64
|
||||
UserID int64
|
||||
RepositoryID int64
|
||||
MilestoneID int64
|
||||
CreatedAfterUnix int64
|
||||
CreatedBeforeUnix int64
|
||||
}
|
||||
|
||||
// toCond will convert each condition into a xorm-Cond
|
||||
func (opts *FindTrackedTimesOptions) toCond() builder.Cond {
|
||||
cond := builder.NewCond().And(builder.Eq{"tracked_time.deleted": false})
|
||||
if opts.IssueID != 0 {
|
||||
cond = cond.And(builder.Eq{"issue_id": opts.IssueID})
|
||||
}
|
||||
if opts.UserID != 0 {
|
||||
cond = cond.And(builder.Eq{"user_id": opts.UserID})
|
||||
}
|
||||
if opts.RepositoryID != 0 {
|
||||
cond = cond.And(builder.Eq{"issue.repo_id": opts.RepositoryID})
|
||||
}
|
||||
if opts.MilestoneID != 0 {
|
||||
cond = cond.And(builder.Eq{"issue.milestone_id": opts.MilestoneID})
|
||||
}
|
||||
if opts.CreatedAfterUnix != 0 {
|
||||
cond = cond.And(builder.Gte{"tracked_time.created_unix": opts.CreatedAfterUnix})
|
||||
}
|
||||
if opts.CreatedBeforeUnix != 0 {
|
||||
cond = cond.And(builder.Lte{"tracked_time.created_unix": opts.CreatedBeforeUnix})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
// toSession will convert the given options to a xorm Session by using the conditions from toCond and joining with issue table if required
|
||||
func (opts *FindTrackedTimesOptions) toSession(e db.Engine) db.Engine {
|
||||
sess := e
|
||||
if opts.RepositoryID > 0 || opts.MilestoneID > 0 {
|
||||
sess = e.Join("INNER", "issue", "issue.id = tracked_time.issue_id")
|
||||
}
|
||||
|
||||
sess = sess.Where(opts.toCond())
|
||||
|
||||
if opts.Page != 0 {
|
||||
sess = db.SetEnginePagination(sess, opts)
|
||||
}
|
||||
|
||||
return sess
|
||||
}
|
||||
|
||||
// GetTrackedTimes returns all tracked times that fit to the given options.
|
||||
func GetTrackedTimes(ctx context.Context, options *FindTrackedTimesOptions) (trackedTimes TrackedTimeList, err error) {
|
||||
err = options.toSession(db.GetEngine(ctx)).Find(&trackedTimes)
|
||||
return
|
||||
}
|
||||
|
||||
// CountTrackedTimes returns count of tracked times that fit to the given options.
|
||||
func CountTrackedTimes(opts *FindTrackedTimesOptions) (int64, error) {
|
||||
sess := db.GetEngine(db.DefaultContext).Where(opts.toCond())
|
||||
if opts.RepositoryID > 0 || opts.MilestoneID > 0 {
|
||||
sess = sess.Join("INNER", "issue", "issue.id = tracked_time.issue_id")
|
||||
}
|
||||
return sess.Count(&TrackedTime{})
|
||||
}
|
||||
|
||||
// GetTrackedSeconds return sum of seconds
|
||||
func GetTrackedSeconds(ctx context.Context, opts FindTrackedTimesOptions) (trackedSeconds int64, err error) {
|
||||
return opts.toSession(db.GetEngine(ctx)).SumInt(&TrackedTime{}, "time")
|
||||
}
|
||||
|
||||
// AddTime will add the given time (in seconds) to the issue
|
||||
func AddTime(user *user_model.User, issue *Issue, amount int64, created time.Time) (*TrackedTime, error) {
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
t, err := addTime(ctx, user, issue, amount, created)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{
|
||||
Issue: issue,
|
||||
Repo: issue.Repo,
|
||||
Doer: user,
|
||||
Content: util.SecToTime(amount),
|
||||
Type: CommentTypeAddTimeManual,
|
||||
TimeID: t.ID,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t, committer.Commit()
|
||||
}
|
||||
|
||||
func addTime(ctx context.Context, user *user_model.User, issue *Issue, amount int64, created time.Time) (*TrackedTime, error) {
|
||||
if created.IsZero() {
|
||||
created = time.Now()
|
||||
}
|
||||
tt := &TrackedTime{
|
||||
IssueID: issue.ID,
|
||||
UserID: user.ID,
|
||||
Time: amount,
|
||||
Created: created,
|
||||
}
|
||||
return tt, db.Insert(ctx, tt)
|
||||
}
|
||||
|
||||
// TotalTimes returns the spent time for each user by an issue
|
||||
func TotalTimes(options *FindTrackedTimesOptions) (map[*user_model.User]string, error) {
|
||||
trackedTimes, err := GetTrackedTimes(db.DefaultContext, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Adding total time per user ID
|
||||
totalTimesByUser := make(map[int64]int64)
|
||||
for _, t := range trackedTimes {
|
||||
totalTimesByUser[t.UserID] += t.Time
|
||||
}
|
||||
|
||||
totalTimes := make(map[*user_model.User]string)
|
||||
// Fetching User and making time human readable
|
||||
for userID, total := range totalTimesByUser {
|
||||
user, err := user_model.GetUserByID(userID)
|
||||
if err != nil {
|
||||
if user_model.IsErrUserNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
totalTimes[user] = util.SecToTime(total)
|
||||
}
|
||||
return totalTimes, nil
|
||||
}
|
||||
|
||||
// DeleteIssueUserTimes deletes times for issue
|
||||
func DeleteIssueUserTimes(issue *Issue, user *user_model.User) error {
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
opts := FindTrackedTimesOptions{
|
||||
IssueID: issue.ID,
|
||||
UserID: user.ID,
|
||||
}
|
||||
|
||||
removedTime, err := deleteTimes(ctx, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if removedTime == 0 {
|
||||
return db.ErrNotExist{}
|
||||
}
|
||||
|
||||
if err := issue.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{
|
||||
Issue: issue,
|
||||
Repo: issue.Repo,
|
||||
Doer: user,
|
||||
Content: "- " + util.SecToTime(removedTime),
|
||||
Type: CommentTypeDeleteTimeManual,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// DeleteTime delete a specific Time
|
||||
func DeleteTime(t *TrackedTime) error {
|
||||
ctx, committer, err := db.TxContext()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if err := t.loadAttributes(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := deleteTime(ctx, t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := CreateCommentCtx(ctx, &CreateCommentOptions{
|
||||
Issue: t.Issue,
|
||||
Repo: t.Issue.Repo,
|
||||
Doer: t.User,
|
||||
Content: "- " + util.SecToTime(t.Time),
|
||||
Type: CommentTypeDeleteTimeManual,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
func deleteTimes(ctx context.Context, opts FindTrackedTimesOptions) (removedTime int64, err error) {
|
||||
removedTime, err = GetTrackedSeconds(ctx, opts)
|
||||
if err != nil || removedTime == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = opts.toSession(db.GetEngine(ctx)).Table("tracked_time").Cols("deleted").Update(&TrackedTime{Deleted: true})
|
||||
return
|
||||
}
|
||||
|
||||
func deleteTime(ctx context.Context, t *TrackedTime) error {
|
||||
if t.Deleted {
|
||||
return db.ErrNotExist{ID: t.ID}
|
||||
}
|
||||
t.Deleted = true
|
||||
_, err := db.GetEngine(ctx).ID(t.ID).Cols("deleted").Update(t)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetTrackedTimeByID returns raw TrackedTime without loading attributes by id
|
||||
func GetTrackedTimeByID(id int64) (*TrackedTime, error) {
|
||||
time := new(TrackedTime)
|
||||
has, err := db.GetEngine(db.DefaultContext).ID(id).Get(time)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, db.ErrNotExist{ID: id}
|
||||
}
|
||||
return time, nil
|
||||
}
|
118
models/issues/tracked_time_test.go
Normal file
118
models/issues/tracked_time_test.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||
// Use of this source code is governed by a MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package issues_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAddTime(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user3, err := user_model.GetUserByID(3)
|
||||
assert.NoError(t, err)
|
||||
|
||||
issue1, err := issues_model.GetIssueByID(db.DefaultContext, 1)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 3661 = 1h 1min 1s
|
||||
trackedTime, err := issues_model.AddTime(user3, issue1, 3661, time.Now())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(3), trackedTime.UserID)
|
||||
assert.Equal(t, int64(1), trackedTime.IssueID)
|
||||
assert.Equal(t, int64(3661), trackedTime.Time)
|
||||
|
||||
tt := unittest.AssertExistsAndLoadBean(t, &issues_model.TrackedTime{UserID: 3, IssueID: 1}).(*issues_model.TrackedTime)
|
||||
assert.Equal(t, int64(3661), tt.Time)
|
||||
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeAddTimeManual, PosterID: 3, IssueID: 1}).(*issues_model.Comment)
|
||||
assert.Equal(t, comment.Content, "1 hour 1 minute")
|
||||
}
|
||||
|
||||
func TestGetTrackedTimes(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// by Issue
|
||||
times, err := issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{IssueID: 1})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, times, 1)
|
||||
assert.Equal(t, int64(400), times[0].Time)
|
||||
|
||||
times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{IssueID: -1})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, times, 0)
|
||||
|
||||
// by User
|
||||
times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{UserID: 1})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, times, 3)
|
||||
assert.Equal(t, int64(400), times[0].Time)
|
||||
|
||||
times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{UserID: 3})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, times, 0)
|
||||
|
||||
// by Repo
|
||||
times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 2})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, times, 3)
|
||||
assert.Equal(t, int64(1), times[0].Time)
|
||||
issue, err := issues_model.GetIssueByID(db.DefaultContext, times[0].IssueID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, issue.RepoID, int64(2))
|
||||
|
||||
times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 1})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, times, 5)
|
||||
|
||||
times, err = issues_model.GetTrackedTimes(db.DefaultContext, &issues_model.FindTrackedTimesOptions{RepositoryID: 10})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, times, 0)
|
||||
}
|
||||
|
||||
func TestTotalTimes(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
total, err := issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 1})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, total, 1)
|
||||
for user, time := range total {
|
||||
assert.Equal(t, int64(1), user.ID)
|
||||
assert.Equal(t, "6 minutes 40 seconds", time)
|
||||
}
|
||||
|
||||
total, err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 2})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, total, 2)
|
||||
for user, time := range total {
|
||||
if user.ID == 2 {
|
||||
assert.Equal(t, "1 hour 1 minute", time)
|
||||
} else if user.ID == 1 {
|
||||
assert.Equal(t, "20 seconds", time)
|
||||
} else {
|
||||
assert.Error(t, assert.AnError)
|
||||
}
|
||||
}
|
||||
|
||||
total, err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 5})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, total, 1)
|
||||
for user, time := range total {
|
||||
assert.Equal(t, int64(2), user.ID)
|
||||
assert.Equal(t, "1 second", time)
|
||||
}
|
||||
|
||||
total, err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 4})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, total, 2)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue