1
1
mirror of https://github.com/go-gitea/gitea synced 2025-07-22 18:28:37 +00:00

Merge branch 'master' into feat/approval-new

# Conflicts:
#	models/issue_comment.go
#	models/migrations/migrations.go
#	models/migrations/v64.go
#	models/models.go
#	public/css/index.css
This commit is contained in:
Jonas Franz
2018-05-13 22:14:34 +02:00
95 changed files with 4325 additions and 927 deletions

View File

@@ -733,6 +733,22 @@ func (err ErrRepoFileAlreadyExist) Error() string {
return fmt.Sprintf("repository file already exists [file_name: %s]", err.FileName)
}
// ErrUserDoesNotHaveAccessToRepo represets an error where the user doesn't has access to a given repo
type ErrUserDoesNotHaveAccessToRepo struct {
UserID int64
RepoName string
}
// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExist.
func IsErrUserDoesNotHaveAccessToRepo(err error) bool {
_, ok := err.(ErrUserDoesNotHaveAccessToRepo)
return ok
}
func (err ErrUserDoesNotHaveAccessToRepo) Error() string {
return fmt.Sprintf("user doesn't have acces to repo [user_id: %d, repo_name: %s]", err.UserID, err.RepoName)
}
// __________ .__
// \______ \____________ ____ ____ | |__
// | | _/\_ __ \__ \ / \_/ ___\| | \

View File

@@ -3,7 +3,6 @@
repo_id: 1
index: 1
poster_id: 1
assignee_id: 1
name: issue1
content: content for the first issue
is_closed: false
@@ -67,7 +66,6 @@
repo_id: 3
index: 1
poster_id: 1
assignee_id: 1
name: issue6
content: content6
is_closed: false

View File

@@ -0,0 +1,8 @@
-
id: 1
assignee_id: 1
issue_id: 1
-
id: 2
assignee_id: 1
issue_id: 6

View File

@@ -3,7 +3,6 @@
uid: 1
issue_id: 1
is_read: true
is_assigned: true
is_mentioned: false
-
@@ -11,7 +10,6 @@
uid: 2
issue_id: 1
is_read: true
is_assigned: false
is_mentioned: false
-
@@ -19,5 +17,4 @@
uid: 4
issue_id: 1
is_read: false
is_assigned: false
is_mentioned: false

View File

@@ -0,0 +1 @@
[] # empty

View File

@@ -37,7 +37,7 @@ type Issue struct {
MilestoneID int64 `xorm:"INDEX"`
Milestone *Milestone `xorm:"-"`
Priority int
AssigneeID int64 `xorm:"INDEX"`
AssigneeID int64 `xorm:"-"`
Assignee *User `xorm:"-"`
IsClosed bool `xorm:"INDEX"`
IsRead bool `xorm:"-"`
@@ -56,6 +56,7 @@ type Issue struct {
Comments []*Comment `xorm:"-"`
Reactions ReactionList `xorm:"-"`
TotalTrackedTime int64 `xorm:"-"`
Assignees []*User `xorm:"-"`
}
var (
@@ -140,22 +141,6 @@ func (issue *Issue) loadPoster(e Engine) (err error) {
return
}
func (issue *Issue) loadAssignee(e Engine) (err error) {
if issue.Assignee == nil && issue.AssigneeID > 0 {
issue.Assignee, err = getUserByID(e, issue.AssigneeID)
if err != nil {
issue.AssigneeID = -1
issue.Assignee = NewGhostUser()
if !IsErrUserNotExist(err) {
return fmt.Errorf("getUserByID.(assignee) [%d]: %v", issue.AssigneeID, err)
}
err = nil
return
}
}
return
}
func (issue *Issue) loadPullRequest(e Engine) (err error) {
if issue.IsPull && issue.PullRequest == nil {
issue.PullRequest, err = getPullRequestByIssueID(e, issue.ID)
@@ -231,7 +216,7 @@ func (issue *Issue) loadAttributes(e Engine) (err error) {
}
}
if err = issue.loadAssignee(e); err != nil {
if err = issue.loadAssignees(e); err != nil {
return
}
@@ -343,8 +328,11 @@ func (issue *Issue) APIFormat() *api.Issue {
if issue.Milestone != nil {
apiIssue.Milestone = issue.Milestone.APIFormat()
}
if issue.Assignee != nil {
apiIssue.Assignee = issue.Assignee.APIFormat()
if len(issue.Assignees) > 0 {
for _, assignee := range issue.Assignees {
apiIssue.Assignees = append(apiIssue.Assignees, assignee.APIFormat())
}
apiIssue.Assignee = issue.Assignees[0].APIFormat() // For compatibility, we're keeping the first assignee as `apiIssue.Assignee`
}
if issue.IsPull {
apiIssue.PullRequest = &api.PullRequestMeta{
@@ -605,19 +593,6 @@ func (issue *Issue) ReplaceLabels(labels []*Label, doer *User) (err error) {
return sess.Commit()
}
// GetAssignee sets the Assignee attribute of this issue.
func (issue *Issue) GetAssignee() (err error) {
if issue.AssigneeID == 0 || issue.Assignee != nil {
return nil
}
issue.Assignee, err = GetUserByID(issue.AssigneeID)
if IsErrUserNotExist(err) {
return nil
}
return err
}
// ReadBy sets issue to be read by given user.
func (issue *Issue) ReadBy(userID int64) error {
if err := UpdateIssueUserByRead(userID, issue.ID); err != nil {
@@ -823,55 +798,6 @@ func (issue *Issue) ChangeContent(doer *User, content string) (err error) {
return nil
}
// ChangeAssignee changes the Assignee field of this issue.
func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) {
var oldAssigneeID = issue.AssigneeID
issue.AssigneeID = assigneeID
if err = UpdateIssueUserByAssignee(issue); err != nil {
return fmt.Errorf("UpdateIssueUserByAssignee: %v", err)
}
sess := x.NewSession()
defer sess.Close()
if err = issue.loadRepo(sess); err != nil {
return fmt.Errorf("loadRepo: %v", err)
}
if _, err = createAssigneeComment(sess, doer, issue.Repo, issue, oldAssigneeID, assigneeID); err != nil {
return fmt.Errorf("createAssigneeComment: %v", err)
}
issue.Assignee, err = GetUserByID(issue.AssigneeID)
if err != nil && !IsErrUserNotExist(err) {
log.Error(4, "GetUserByID [assignee_id: %v]: %v", issue.AssigneeID, err)
return nil
}
// Error not nil here means user does not exist, which is remove assignee.
isRemoveAssignee := err != nil
if issue.IsPull {
issue.PullRequest.Issue = issue
apiPullRequest := &api.PullRequestPayload{
Index: issue.Index,
PullRequest: issue.PullRequest.APIFormat(),
Repository: issue.Repo.APIFormat(AccessModeNone),
Sender: doer.APIFormat(),
}
if isRemoveAssignee {
apiPullRequest.Action = api.HookIssueUnassigned
} else {
apiPullRequest.Action = api.HookIssueAssigned
}
if err := PrepareWebhooks(issue.Repo, HookEventPullRequest, apiPullRequest); err != nil {
log.Error(4, "PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, isRemoveAssignee, err)
return nil
}
}
go HookQueue.Add(issue.RepoID)
return nil
}
// GetTasks returns the amount of tasks in the issues content
func (issue *Issue) GetTasks() int {
return len(issueTasksPat.FindAllStringIndex(issue.Content, -1))
@@ -887,6 +813,7 @@ type NewIssueOptions struct {
Repo *Repository
Issue *Issue
LabelIDs []int64
AssigneeIDs []int64
Attachments []string // In UUID format.
IsPull bool
}
@@ -909,14 +836,32 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
}
}
if assigneeID := opts.Issue.AssigneeID; assigneeID > 0 {
valid, err := hasAccess(e, assigneeID, opts.Repo, AccessModeWrite)
if err != nil {
return fmt.Errorf("hasAccess [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err)
// Keep the old assignee id thingy for compatibility reasons
if opts.Issue.AssigneeID > 0 {
isAdded := false
// Check if the user has already been passed to issue.AssigneeIDs, if not, add it
for _, aID := range opts.AssigneeIDs {
if aID == opts.Issue.AssigneeID {
isAdded = true
break
}
}
if !valid {
opts.Issue.AssigneeID = 0
opts.Issue.Assignee = nil
if !isAdded {
opts.AssigneeIDs = append(opts.AssigneeIDs, opts.Issue.AssigneeID)
}
}
// Check for and validate assignees
if len(opts.AssigneeIDs) > 0 {
for _, assigneeID := range opts.AssigneeIDs {
valid, err := hasAccess(e, assigneeID, opts.Repo, AccessModeWrite)
if err != nil {
return fmt.Errorf("hasAccess [user_id: %d, repo_id: %d]: %v", assigneeID, opts.Repo.ID, err)
}
if !valid {
return ErrUserDoesNotHaveAccessToRepo{UserID: assigneeID, RepoName: opts.Repo.Name}
}
}
}
@@ -931,11 +876,10 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
}
}
if opts.Issue.AssigneeID > 0 {
if err = opts.Issue.loadRepo(e); err != nil {
return err
}
if _, err = createAssigneeComment(e, doer, opts.Issue.Repo, opts.Issue, -1, opts.Issue.AssigneeID); err != nil {
// Insert the assignees
for _, assigneeID := range opts.AssigneeIDs {
err = opts.Issue.changeAssignee(e, doer, assigneeID)
if err != nil {
return err
}
}
@@ -995,7 +939,7 @@ func newIssue(e *xorm.Session, doer *User, opts NewIssueOptions) (err error) {
}
// NewIssue creates new issue with labels for repository.
func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string) (err error) {
func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, assigneeIDs []int64, uuids []string) (err error) {
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
@@ -1007,7 +951,11 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string)
Issue: issue,
LabelIDs: labelIDs,
Attachments: uuids,
AssigneeIDs: assigneeIDs,
}); err != nil {
if IsErrUserDoesNotHaveAccessToRepo(err) {
return err
}
return fmt.Errorf("newIssue: %v", err)
}
@@ -1150,7 +1098,8 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) error {
}
if opts.AssigneeID > 0 {
sess.And("issue.assignee_id=?", opts.AssigneeID)
sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
And("issue_assignees.assignee_id = ?", opts.AssigneeID)
}
if opts.PosterID > 0 {
@@ -1372,7 +1321,8 @@ func GetIssueStats(opts *IssueStatsOptions) (*IssueStats, error) {
}
if opts.AssigneeID > 0 {
sess.And("issue.assignee_id = ?", opts.AssigneeID)
sess.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
And("issue_assignees.assignee_id = ?", opts.AssigneeID)
}
if opts.PosterID > 0 {
@@ -1438,13 +1388,15 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) {
}
case FilterModeAssign:
stats.OpenCount, err = x.Where(cond).And("is_closed = ?", false).
And("assignee_id = ?", opts.UserID).
Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
And("issue_assignees.assignee_id = ?", opts.UserID).
Count(new(Issue))
if err != nil {
return nil, err
}
stats.ClosedCount, err = x.Where(cond).And("is_closed = ?", true).
And("assignee_id = ?", opts.UserID).
Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
And("issue_assignees.assignee_id = ?", opts.UserID).
Count(new(Issue))
if err != nil {
return nil, err
@@ -1466,7 +1418,8 @@ func GetUserIssueStats(opts UserIssueStatsOptions) (*IssueStats, error) {
cond = cond.And(builder.Eq{"issue.is_closed": opts.IsClosed})
stats.AssignCount, err = x.Where(cond).
And("assignee_id = ?", opts.UserID).
Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
And("issue_assignees.assignee_id = ?", opts.UserID).
Count(new(Issue))
if err != nil {
return nil, err
@@ -1505,8 +1458,10 @@ func GetRepoIssueStats(repoID, uid int64, filterMode int, isPull bool) (numOpen
switch filterMode {
case FilterModeAssign:
openCountSession.And("assignee_id = ?", uid)
closedCountSession.And("assignee_id = ?", uid)
openCountSession.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
And("issue_assignees.assignee_id = ?", uid)
closedCountSession.Join("INNER", "issue_assignees", "issue.id = issue_assignees.issue_id").
And("issue_assignees.assignee_id = ?", uid)
case FilterModeCreate:
openCountSession.And("poster_id = ?", uid)
closedCountSession.And("poster_id = ?", uid)

263
models/issue_assignees.go Normal file
View File

@@ -0,0 +1,263 @@
// 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 models
import (
"fmt"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/sdk/gitea"
"github.com/go-xorm/xorm"
)
// IssueAssignees saves all issue assignees
type IssueAssignees struct {
ID int64 `xorm:"pk autoincr"`
AssigneeID int64 `xorm:"INDEX"`
IssueID int64 `xorm:"INDEX"`
}
// This loads all assignees of an issue
func (issue *Issue) loadAssignees(e Engine) (err error) {
// Reset maybe preexisting assignees
issue.Assignees = []*User{}
err = e.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
}
// GetAssigneesByIssue returns everyone assigned to that issue
func GetAssigneesByIssue(issue *Issue) (assignees []*User, err error) {
err = issue.loadAssignees(x)
if err != nil {
return assignees, err
}
return issue.Assignees, nil
}
// IsUserAssignedToIssue returns true when the user is assigned to the issue
func IsUserAssignedToIssue(issue *Issue, user *User) (isAssigned bool, err error) {
isAssigned, err = x.Exist(&IssueAssignees{IssueID: issue.ID, AssigneeID: user.ID})
return
}
// DeleteNotPassedAssignee deletes all assignees who aren't passed via the "assignees" array
func DeleteNotPassedAssignee(issue *Issue, doer *User, assignees []*User) (err error) {
var found bool
for _, assignee := range issue.Assignees {
found = false
for _, alreadyAssignee := range assignees {
if assignee.ID == alreadyAssignee.ID {
found = true
break
}
}
if !found {
// This function also does comments and hooks, which is why we call it seperatly instead of directly removing the assignees here
if err := UpdateAssignee(issue, doer, assignee.ID); err != nil {
return err
}
}
}
return nil
}
// MakeAssigneeList concats a string with all names of the assignees. Useful for logs.
func MakeAssigneeList(issue *Issue) (assigneeList string, err error) {
err = issue.loadAssignees(x)
if err != nil {
return "", err
}
for in, assignee := range issue.Assignees {
assigneeList += assignee.Name
if len(issue.Assignees) > (in + 1) {
assigneeList += ", "
}
}
return
}
// ClearAssigneeByUserID deletes all assignments of an user
func clearAssigneeByUserID(sess *xorm.Session, userID int64) (err error) {
_, err = sess.Delete(&IssueAssignees{AssigneeID: userID})
return
}
// AddAssigneeIfNotAssigned adds an assignee only if he isn't aleady assigned to the issue
func AddAssigneeIfNotAssigned(issue *Issue, doer *User, assigneeID int64) (err error) {
// Check if the user is already assigned
isAssigned, err := IsUserAssignedToIssue(issue, &User{ID: assigneeID})
if err != nil {
return err
}
if !isAssigned {
return issue.ChangeAssignee(doer, assigneeID)
}
return nil
}
// UpdateAssignee deletes or adds an assignee to an issue
func UpdateAssignee(issue *Issue, doer *User, assigneeID int64) (err error) {
return issue.ChangeAssignee(doer, assigneeID)
}
// ChangeAssignee changes the Assignee of this issue.
func (issue *Issue) ChangeAssignee(doer *User, assigneeID int64) (err error) {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if err := issue.changeAssignee(sess, doer, assigneeID); err != nil {
return err
}
return sess.Commit()
}
func (issue *Issue) changeAssignee(sess *xorm.Session, doer *User, assigneeID int64) (err error) {
// Update the assignee
removed, err := updateIssueAssignee(sess, issue, assigneeID)
if err != nil {
return fmt.Errorf("UpdateIssueUserByAssignee: %v", err)
}
// Repo infos
if err = issue.loadRepo(sess); err != nil {
return fmt.Errorf("loadRepo: %v", err)
}
// Comment
if _, err = createAssigneeComment(sess, doer, issue.Repo, issue, assigneeID, removed); err != nil {
return fmt.Errorf("createAssigneeComment: %v", err)
}
if issue.IsPull {
issue.PullRequest = &PullRequest{Issue: issue}
apiPullRequest := &api.PullRequestPayload{
Index: issue.Index,
PullRequest: issue.PullRequest.APIFormat(),
Repository: issue.Repo.APIFormat(AccessModeNone),
Sender: doer.APIFormat(),
}
if removed {
apiPullRequest.Action = api.HookIssueUnassigned
} else {
apiPullRequest.Action = api.HookIssueAssigned
}
if err := PrepareWebhooks(issue.Repo, HookEventPullRequest, apiPullRequest); err != nil {
log.Error(4, "PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, removed, err)
return nil
}
}
go HookQueue.Add(issue.RepoID)
return nil
}
// UpdateAPIAssignee is a helper function to add or delete one or multiple issue assignee(s)
// Deleting is done the Github way (quote from their api documentation):
// https://developer.github.com/v3/issues/#edit-an-issue
// "assignees" (array): Logins for Users to assign to this issue.
// Pass one or more user logins to replace the set of assignees on this Issue.
// Send an empty array ([]) to clear all assignees from the Issue.
func UpdateAPIAssignee(issue *Issue, oneAssignee string, multipleAssignees []string, doer *User) (err error) {
var allNewAssignees []*User
// Keep the old assignee thingy for compatibility reasons
if oneAssignee != "" {
// Prevent double adding assignees
var isDouble bool
for _, assignee := range multipleAssignees {
if assignee == oneAssignee {
isDouble = true
break
}
}
if !isDouble {
multipleAssignees = append(multipleAssignees, oneAssignee)
}
}
// Loop through all assignees to add them
for _, assigneeName := range multipleAssignees {
assignee, err := GetUserByName(assigneeName)
if err != nil {
return err
}
allNewAssignees = append(allNewAssignees, assignee)
}
// Delete all old assignees not passed
if err = DeleteNotPassedAssignee(issue, doer, allNewAssignees); err != nil {
return err
}
// Add all new assignees
// Update the assignee. The function will check if the user exists, is already
// assigned (which he shouldn't as we deleted all assignees before) and
// has access to the repo.
for _, assignee := range allNewAssignees {
// Extra method to prevent double adding (which would result in removing)
err = AddAssigneeIfNotAssigned(issue, doer, assignee.ID)
if err != nil {
return err
}
}
return
}
// MakeIDsFromAPIAssigneesToAdd returns an array with all assignee IDs
func MakeIDsFromAPIAssigneesToAdd(oneAssignee string, multipleAssignees []string) (assigneeIDs []int64, err error) {
// Keeping the old assigning method for compatibility reasons
if oneAssignee != "" {
// Prevent double adding assignees
var isDouble bool
for _, assignee := range multipleAssignees {
if assignee == oneAssignee {
isDouble = true
break
}
}
if !isDouble {
multipleAssignees = append(multipleAssignees, oneAssignee)
}
}
// Get the IDs of all assignees
assigneeIDs = GetUserIDsByNames(multipleAssignees)
return
}

View File

@@ -0,0 +1,71 @@
// 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 models
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestUpdateAssignee(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
// Fake issue with assignees
issue, err := GetIssueByID(1)
assert.NoError(t, err)
// Assign multiple users
user2, err := GetUserByID(2)
assert.NoError(t, err)
err = UpdateAssignee(issue, &User{ID: 1}, user2.ID)
assert.NoError(t, err)
user3, err := GetUserByID(3)
assert.NoError(t, err)
err = UpdateAssignee(issue, &User{ID: 1}, user3.ID)
assert.NoError(t, err)
user1, err := GetUserByID(1) // This user is already assigned (see the definition in fixtures), so running UpdateAssignee should unassign him
assert.NoError(t, err)
err = UpdateAssignee(issue, &User{ID: 1}, user1.ID)
assert.NoError(t, err)
// Check if he got removed
isAssigned, err := IsUserAssignedToIssue(issue, user1)
assert.NoError(t, err)
assert.False(t, isAssigned)
// Check if they're all there
assignees, err := GetAssigneesByIssue(issue)
assert.NoError(t, err)
var expectedAssignees []*User
expectedAssignees = append(expectedAssignees, user2)
expectedAssignees = append(expectedAssignees, user3)
for in, assignee := range assignees {
assert.Equal(t, assignee.ID, expectedAssignees[in].ID)
}
// Check if the user is assigned
isAssigned, err = IsUserAssignedToIssue(issue, user2)
assert.NoError(t, err)
assert.True(t, isAssigned)
// This user should not be assigned
isAssigned, err = IsUserAssignedToIssue(issue, &User{ID: 4})
assert.NoError(t, err)
assert.False(t, isAssigned)
// Clean everyone
err = DeleteNotPassedAssignee(issue, user1, []*User{})
assert.NoError(t, err)
// Check they're gone
assignees, err = GetAssigneesByIssue(issue)
assert.NoError(t, err)
assert.Equal(t, 0, len(assignees))
}

View File

@@ -88,23 +88,22 @@ const (
// Comment represents a comment in commit and issue page.
type Comment struct {
ID int64 `xorm:"pk autoincr"`
Type CommentType
PosterID int64 `xorm:"INDEX"`
Poster *User `xorm:"-"`
IssueID int64 `xorm:"INDEX"`
LabelID int64
Label *Label `xorm:"-"`
OldMilestoneID int64
MilestoneID int64
OldMilestone *Milestone `xorm:"-"`
Milestone *Milestone `xorm:"-"`
OldAssigneeID int64
AssigneeID int64
Assignee *User `xorm:"-"`
OldAssignee *User `xorm:"-"`
OldTitle string
NewTitle string
ID int64 `xorm:"pk autoincr"`
Type CommentType
PosterID int64 `xorm:"INDEX"`
Poster *User `xorm:"-"`
IssueID int64 `xorm:"INDEX"`
LabelID int64
Label *Label `xorm:"-"`
OldMilestoneID int64
MilestoneID int64
OldMilestone *Milestone `xorm:"-"`
Milestone *Milestone `xorm:"-"`
AssigneeID int64
RemovedAssignee bool
Assignee *User `xorm:"-"`
OldTitle string
NewTitle string
CommitID int64
Line int64 // - previous line / + proposed line
@@ -259,18 +258,9 @@ func (c *Comment) LoadMilestone() error {
return nil
}
// LoadAssignees if comment.Type is CommentTypeAssignees, then load assignees
func (c *Comment) LoadAssignees() error {
// LoadAssigneeUser if comment.Type is CommentTypeAssignees, then load assignees
func (c *Comment) LoadAssigneeUser() error {
var err error
if c.OldAssigneeID > 0 {
c.OldAssignee, err = getUserByID(x, c.OldAssigneeID)
if err != nil {
if !IsErrUserNotExist(err) {
return err
}
c.OldAssignee = NewGhostUser()
}
}
if c.AssigneeID > 0 {
c.Assignee, err = getUserByID(x, c.AssigneeID)
@@ -382,21 +372,21 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
LabelID = opts.Label.ID
}
comment := &Comment{
Type: opts.Type,
PosterID: opts.Doer.ID,
Poster: opts.Doer,
IssueID: opts.Issue.ID,
LabelID: LabelID,
OldMilestoneID: opts.OldMilestoneID,
MilestoneID: opts.MilestoneID,
OldAssigneeID: opts.OldAssigneeID,
AssigneeID: opts.AssigneeID,
CommitID: opts.CommitID,
CommitSHA: opts.CommitSHA,
Line: opts.LineNum,
Content: opts.Content,
OldTitle: opts.OldTitle,
NewTitle: opts.NewTitle,
Type: opts.Type,
PosterID: opts.Doer.ID,
Poster: opts.Doer,
IssueID: opts.Issue.ID,
LabelID: LabelID,
OldMilestoneID: opts.OldMilestoneID,
MilestoneID: opts.MilestoneID,
RemovedAssignee: opts.RemovedAssignee,
AssigneeID: opts.AssigneeID,
CommitID: opts.CommitID,
CommitSHA: opts.CommitSHA,
Line: opts.LineNum,
Content: opts.Content,
OldTitle: opts.OldTitle,
NewTitle: opts.NewTitle,
TreePath: opts.TreePath,
ReviewID: opts.ReviewID,
}
@@ -540,14 +530,14 @@ func createMilestoneComment(e *xorm.Session, doer *User, repo *Repository, issue
})
}
func createAssigneeComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, oldAssigneeID, assigneeID int64) (*Comment, error) {
func createAssigneeComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, assigneeID int64, removedAssignee bool) (*Comment, error) {
return createComment(e, &CreateCommentOptions{
Type: CommentTypeAssignees,
Doer: doer,
Repo: repo,
Issue: issue,
OldAssigneeID: oldAssigneeID,
AssigneeID: assigneeID,
Type: CommentTypeAssignees,
Doer: doer,
Repo: repo,
Issue: issue,
RemovedAssignee: removedAssignee,
AssigneeID: assigneeID,
})
}
@@ -608,19 +598,19 @@ type CreateCommentOptions struct {
Issue *Issue
Label *Label
OldMilestoneID int64
MilestoneID int64
OldAssigneeID int64
AssigneeID int64
OldTitle string
NewTitle string
CommitID int64
CommitSHA string
LineNum int64
OldMilestoneID int64
MilestoneID int64
AssigneeID int64
RemovedAssignee bool
OldTitle string
NewTitle string
CommitID int64
CommitSHA string
LineNum int64
TreePath string
ReviewID int64
Content string
Attachments []string // UUIDs of attachments
Content string
Attachments []string // UUIDs of attachments
}
// CreateComment creates comment of issue or commit.

View File

@@ -154,38 +154,38 @@ func (issues IssueList) loadMilestones(e Engine) error {
return nil
}
func (issues IssueList) getAssigneeIDs() []int64 {
var ids = make(map[int64]struct{}, len(issues))
for _, issue := range issues {
if _, ok := ids[issue.AssigneeID]; !ok {
ids[issue.AssigneeID] = struct{}{}
}
}
return keysInt64(ids)
}
func (issues IssueList) loadAssignees(e Engine) error {
assigneeIDs := issues.getAssigneeIDs()
if len(assigneeIDs) == 0 {
if len(issues) == 0 {
return nil
}
assigneeMaps := make(map[int64]*User, len(assigneeIDs))
err := e.
In("id", assigneeIDs).
Find(&assigneeMaps)
type AssigneeIssue struct {
IssueAssignee *IssueAssignees `xorm:"extends"`
Assignee *User `xorm:"extends"`
}
var assignees = make(map[int64][]*User, len(issues))
rows, err := e.Table("issue_assignees").
Join("INNER", "user", "`user`.id = `issue_assignees`.assignee_id").
In("`issue_assignees`.issue_id", issues.getIssueIDs()).
Rows(new(AssigneeIssue))
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var assigneeIssue AssigneeIssue
err = rows.Scan(&assigneeIssue)
if err != nil {
return err
}
assignees[assigneeIssue.IssueAssignee.IssueID] = append(assignees[assigneeIssue.IssueAssignee.IssueID], assigneeIssue.Assignee)
}
for _, issue := range issues {
if issue.AssigneeID <= 0 {
continue
}
var ok bool
if issue.Assignee, ok = assigneeMaps[issue.AssigneeID]; !ok {
issue.Assignee = NewGhostUser()
}
issue.Assignees = assignees[issue.ID]
}
return nil
}

View File

@@ -46,9 +46,16 @@ func mailIssueCommentToParticipants(e Engine, issue *Issue, doer *User, content
participants = append(participants, issue.Poster)
}
// Assignee must receive any communications
if issue.Assignee != nil && issue.AssigneeID > 0 && issue.AssigneeID != doer.ID {
participants = append(participants, issue.Assignee)
// Assignees must receive any communications
assignees, err := GetAssigneesByIssue(issue)
if err != nil {
return err
}
for _, assignee := range assignees {
if assignee.ID != doer.ID {
participants = append(participants, assignee)
}
}
tos := make([]string, 0, len(watchers)) // List of email addresses.

View File

@@ -24,7 +24,7 @@ type Milestone struct {
NumClosedIssues int
NumOpenIssues int `xorm:"-"`
Completeness int // Percentage(1-100).
IsOverDue bool `xorm:"-"`
IsOverdue bool `xorm:"-"`
DeadlineString string `xorm:"-"`
DeadlineUnix util.TimeStamp
@@ -52,7 +52,7 @@ func (m *Milestone) AfterLoad() {
m.DeadlineString = m.DeadlineUnix.Format("2006-01-02")
if util.TimeStampNow() >= m.DeadlineUnix {
m.IsOverDue = true
m.IsOverdue = true
}
}

View File

@@ -6,6 +6,8 @@ package models
import (
"fmt"
"github.com/go-xorm/xorm"
)
// IssueUser represents an issue-user relation.
@@ -14,7 +16,6 @@ type IssueUser struct {
UID int64 `xorm:"INDEX"` // User ID.
IssueID int64
IsRead bool
IsAssigned bool
IsMentioned bool
}
@@ -32,9 +33,8 @@ func newIssueUsers(e Engine, repo *Repository, issue *Issue) error {
issueUsers := make([]*IssueUser, 0, len(assignees)+1)
for _, assignee := range assignees {
issueUsers = append(issueUsers, &IssueUser{
IssueID: issue.ID,
UID: assignee.ID,
IsAssigned: assignee.ID == issue.AssigneeID,
IssueID: issue.ID,
UID: assignee.ID,
})
isPosterAssignee = isPosterAssignee || assignee.ID == issue.PosterID
}
@@ -51,34 +51,38 @@ func newIssueUsers(e Engine, repo *Repository, issue *Issue) error {
return nil
}
func updateIssueUserByAssignee(e Engine, issue *Issue) (err error) {
if _, err = e.Exec("UPDATE `issue_user` SET is_assigned = ? WHERE issue_id = ?", false, issue.ID); err != nil {
return err
func updateIssueAssignee(e *xorm.Session, issue *Issue, assigneeID int64) (removed bool, err error) {
// Check if the user exists
_, err = GetUserByID(assigneeID)
if err != nil {
return false, err
}
// Assignee ID equals to 0 means clear assignee.
if issue.AssigneeID > 0 {
if _, err = e.Exec("UPDATE `issue_user` SET is_assigned = ? WHERE uid = ? AND issue_id = ?", true, issue.AssigneeID, issue.ID); err != nil {
return err
// Check if the submitted user is already assigne, if yes delete him otherwise add him
var toBeDeleted bool
for _, assignee := range issue.Assignees {
if assignee.ID == assigneeID {
toBeDeleted = true
break
}
}
return updateIssue(e, issue)
}
assigneeIn := IssueAssignees{AssigneeID: assigneeID, IssueID: issue.ID}
// UpdateIssueUserByAssignee updates issue-user relation for assignee.
func UpdateIssueUserByAssignee(issue *Issue) (err error) {
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
if toBeDeleted {
_, err = e.Delete(assigneeIn)
if err != nil {
return toBeDeleted, err
}
} else {
_, err = e.Insert(assigneeIn)
if err != nil {
return toBeDeleted, err
}
}
if err = updateIssueUserByAssignee(sess, issue); err != nil {
return err
}
return sess.Commit()
return toBeDeleted, nil
}
// UpdateIssueUserByRead updates issue-user relation for reading.

View File

@@ -32,23 +32,6 @@ func Test_newIssueUsers(t *testing.T) {
AssertExistsAndLoadBean(t, &IssueUser{IssueID: newIssue.ID, UID: repo.OwnerID})
}
func TestUpdateIssueUserByAssignee(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
issue := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)
// artificially change assignee in issue_user table
AssertSuccessfulInsert(t, &IssueUser{IssueID: issue.ID, UID: 5, IsAssigned: true})
_, err := x.Cols("is_assigned").
Update(&IssueUser{IsAssigned: false}, &IssueUser{IssueID: issue.ID, UID: issue.AssigneeID})
assert.NoError(t, err)
assert.NoError(t, UpdateIssueUserByAssignee(issue))
// issue_user table should now be correct again
AssertExistsAndLoadBean(t, &IssueUser{IssueID: issue.ID, UID: issue.AssigneeID}, "is_assigned=1")
AssertExistsAndLoadBean(t, &IssueUser{IssueID: issue.ID, UID: 5}, "is_assigned=0")
}
func TestUpdateIssueUserByRead(t *testing.T) {
assert.NoError(t, PrepareTestDatabase())
issue := AssertExistsAndLoadBean(t, &Issue{ID: 1}).(*Issue)

View File

@@ -181,6 +181,8 @@ var migrations = []Migration{
// v63 -> v64
NewMigration("add language column for user setting", addLanguageSetting),
// v64 -> v65
NewMigration("add multiple assignees", addMultipleAssignees),
// v65 -> v66
NewMigration("add review", addReview),
}
@@ -231,7 +233,7 @@ Please try to upgrade to a lower version (>= v0.6.0) first, then upgrade to curr
return nil
}
func dropTableColumns(x *xorm.Engine, tableName string, columnNames ...string) (err error) {
func dropTableColumns(sess *xorm.Session, tableName string, columnNames ...string) (err error) {
if tableName == "" || len(columnNames) == 0 {
return nil
}
@@ -247,17 +249,10 @@ func dropTableColumns(x *xorm.Engine, tableName string, columnNames ...string) (
}
cols += "DROP COLUMN `" + col + "`"
}
if _, err := x.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil {
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil {
return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err)
}
case setting.UseMSSQL:
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
cols := ""
for _, col := range columnNames {
if cols != "" {

View File

@@ -9,5 +9,15 @@ import (
)
func removeIsOwnerColumnFromOrgUser(x *xorm.Engine) (err error) {
return dropTableColumns(x, "org_user", "is_owner", "num_teams")
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
return err
}
if err := dropTableColumns(sess, "org_user", "is_owner", "num_teams"); err != nil {
return err
}
return sess.Commit()
}

View File

@@ -5,27 +5,127 @@
package migrations
import (
"fmt"
"code.gitea.io/gitea/modules/util"
"github.com/go-xorm/xorm"
)
func addReview(x *xorm.Engine) error {
// Review see models/review.go
type Review struct {
ID int64 `xorm:"pk autoincr"`
Type string
ReviewerID int64 `xorm:"index"`
IssueID int64 `xorm:"index"`
Content string
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
func addMultipleAssignees(x *xorm.Engine) error {
// Redeclare issue struct
type Issue struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
PosterID int64 `xorm:"INDEX"`
Title string `xorm:"name"`
Content string `xorm:"TEXT"`
MilestoneID int64 `xorm:"INDEX"`
Priority int
AssigneeID int64 `xorm:"INDEX"`
IsClosed bool `xorm:"INDEX"`
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
NumComments int
Ref string
DeadlineUnix util.TimeStamp `xorm:"INDEX"`
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
ClosedUnix util.TimeStamp `xorm:"INDEX"`
}
if err := x.Sync2(new(Review)); err != nil {
return fmt.Errorf("Sync2: %v", err)
// Updated the comment table
type Comment struct {
ID int64 `xorm:"pk autoincr"`
Type int
PosterID int64 `xorm:"INDEX"`
IssueID int64 `xorm:"INDEX"`
LabelID int64
OldMilestoneID int64
MilestoneID int64
OldAssigneeID int64
AssigneeID int64
RemovedAssignee bool
OldTitle string
NewTitle string
CommitID int64
Line int64
Content string `xorm:"TEXT"`
RenderedContent string `xorm:"-"`
CreatedUnix util.TimeStamp `xorm:"INDEX created"`
UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
// Reference issue in commit message
CommitSHA string `xorm:"VARCHAR(40)"`
}
return nil
// Create the table
type IssueAssignees struct {
ID int64 `xorm:"pk autoincr"`
AssigneeID int64 `xorm:"INDEX"`
IssueID int64 `xorm:"INDEX"`
}
if err := x.Sync2(IssueAssignees{}); err != nil {
return err
}
if err := x.Sync2(Comment{}); err != nil {
return err
}
// Range over all issues and insert a new entry for each issue/assignee
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
allIssues := []Issue{}
if err := sess.Find(&allIssues); err != nil {
return err
}
for _, issue := range allIssues {
if issue.AssigneeID != 0 {
_, err := sess.Insert(IssueAssignees{IssueID: issue.ID, AssigneeID: issue.AssigneeID})
if err != nil {
sess.Rollback()
return err
}
}
}
// Migrate comments
// First update everything to not have nulls in db
if _, err := sess.Where("type = ?", 9).Cols("removed_assignee").Update(Comment{RemovedAssignee: false}); err != nil {
return err
}
allAssignementComments := []Comment{}
if err := sess.Where("type = ?", 9).Find(&allAssignementComments); err != nil {
return err
}
for _, comment := range allAssignementComments {
// Everytime where OldAssigneeID is > 0, the assignement was removed.
if comment.OldAssigneeID > 0 {
_, err := sess.ID(comment.ID).Update(Comment{RemovedAssignee: true})
if err != nil {
return err
}
}
}
if err := dropTableColumns(sess, "issue", "assignee_id"); err != nil {
return err
}
if err := dropTableColumns(sess, "issue_user", "is_assigned"); err != nil {
return err
}
return sess.Commit()
}

View File

@@ -119,6 +119,7 @@ func init() {
new(RepoIndexerStatus),
new(LFSLock),
new(Reaction),
new(IssueAssignees),
new(Review),
)

View File

@@ -198,6 +198,7 @@ func (pr *PullRequest) APIFormat() *api.PullRequest {
Labels: apiIssue.Labels,
Milestone: apiIssue.Milestone,
Assignee: apiIssue.Assignee,
Assignees: apiIssue.Assignees,
State: apiIssue.State,
Comments: apiIssue.Comments,
HTMLURL: pr.Issue.HTMLURL(),
@@ -719,7 +720,7 @@ func (pr *PullRequest) testPatch() (err error) {
}
// NewPullRequest creates new pull request with labels for repository.
func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest, patch []byte) (err error) {
func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []string, pr *PullRequest, patch []byte, assigneeIDs []int64) (err error) {
sess := x.NewSession()
defer sess.Close()
if err = sess.Begin(); err != nil {
@@ -732,7 +733,11 @@ func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []str
LabelIDs: labelIDs,
Attachments: uuids,
IsPull: true,
AssigneeIDs: assigneeIDs,
}); err != nil {
if IsErrUserDoesNotHaveAccessToRepo(err) {
return err
}
return fmt.Errorf("newIssue: %v", err)
}

View File

@@ -600,9 +600,9 @@ func (repo *Repository) GetAssignees() (_ []*User, err error) {
return repo.getAssignees(x)
}
// GetAssigneeByID returns the user that has write access of repository by given ID.
func (repo *Repository) GetAssigneeByID(userID int64) (*User, error) {
return GetAssigneeByID(repo, userID)
// GetUserIfHasWriteAccess returns the user that has write access of repository by given ID.
func (repo *Repository) GetUserIfHasWriteAccess(userID int64) (*User, error) {
return GetUserIfHasWriteAccess(repo, userID)
}
// GetMilestoneByID returns the milestone belongs to repository by given ID.

View File

@@ -993,7 +993,7 @@ func deleteUser(e *xorm.Session, u *User) error {
// ***** END: PublicKey *****
// Clear assignee.
if _, err = e.Exec("UPDATE `issue` SET assignee_id=0 WHERE assignee_id=?", u.ID); err != nil {
if err = clearAssigneeByUserID(e, u.ID); err != nil {
return fmt.Errorf("clear assignee: %v", err)
}
@@ -1110,8 +1110,8 @@ func GetUserByID(id int64) (*User, error) {
return getUserByID(x, id)
}
// GetAssigneeByID returns the user with write access of repository by given ID.
func GetAssigneeByID(repo *Repository, userID int64) (*User, error) {
// GetUserIfHasWriteAccess returns the user with write access of repository by given ID.
func GetUserIfHasWriteAccess(repo *Repository, userID int64) (*User, error) {
has, err := HasAccess(userID, repo, AccessModeWrite)
if err != nil {
return nil, err

View File

@@ -118,8 +118,12 @@ func getDingtalkPullRequestPayload(p *api.PullRequestPayload) (*DingtalkPayload,
title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
text = p.PullRequest.Body
case api.HookIssueAssigned:
list, err := MakeAssigneeList(&Issue{ID: p.PullRequest.ID})
if err != nil {
return &DingtalkPayload{}, err
}
title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d %s", p.Repository.FullName,
p.PullRequest.Assignee.UserName, p.Index, p.PullRequest.Title)
list, p.Index, p.PullRequest.Title)
text = p.PullRequest.Body
case api.HookIssueUnassigned:
title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)

View File

@@ -191,8 +191,12 @@ func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta)
text = p.PullRequest.Body
color = warnColor
case api.HookIssueAssigned:
list, err := MakeAssigneeList(&Issue{ID: p.PullRequest.ID})
if err != nil {
return &DiscordPayload{}, err
}
title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d %s", p.Repository.FullName,
p.PullRequest.Assignee.UserName, p.Index, p.PullRequest.Title)
list, p.Index, p.PullRequest.Title)
text = p.PullRequest.Body
color = successColor
case api.HookIssueUnassigned:

View File

@@ -172,8 +172,12 @@ func getSlackPullRequestPayload(p *api.PullRequestPayload, slack *SlackMeta) (*S
text = fmt.Sprintf("[%s] Pull request edited: %s by %s", p.Repository.FullName, titleLink, senderLink)
attachmentText = SlackTextFormatter(p.PullRequest.Body)
case api.HookIssueAssigned:
list, err := MakeAssigneeList(&Issue{ID: p.PullRequest.ID})
if err != nil {
return &SlackPayload{}, err
}
text = fmt.Sprintf("[%s] Pull request assigned to %s: %s by %s", p.Repository.FullName,
SlackLinkFormatter(setting.AppURL+p.PullRequest.Assignee.UserName, p.PullRequest.Assignee.UserName),
SlackLinkFormatter(setting.AppURL+list, list),
titleLink, senderLink)
case api.HookIssueUnassigned:
text = fmt.Sprintf("[%s] Pull request unassigned: %s by %s", p.Repository.FullName, titleLink, senderLink)