diff --git a/models/issues/comment.go b/models/issues/comment.go index 17e579b455..e045b71d23 100644 --- a/models/issues/comment.go +++ b/models/issues/comment.go @@ -17,6 +17,7 @@ import ( project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" @@ -1247,3 +1248,44 @@ func FixCommentTypeLabelWithOutsideLabels(ctx context.Context) (int64, error) { func (c *Comment) HasOriginalAuthor() bool { return c.OriginalAuthor != "" && c.OriginalAuthorID != 0 } + +// InsertIssueComments inserts many comments of issues. +func InsertIssueComments(comments []*Comment) error { + if len(comments) == 0 { + return nil + } + + issueIDs := make(container.Set[int64]) + for _, comment := range comments { + issueIDs.Add(comment.IssueID) + } + + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + for _, comment := range comments { + if _, err := db.GetEngine(ctx).NoAutoTime().Insert(comment); err != nil { + return err + } + + for _, reaction := range comment.Reactions { + reaction.IssueID = comment.IssueID + reaction.CommentID = comment.ID + } + if len(comment.Reactions) > 0 { + if err := db.Insert(ctx, comment.Reactions); err != nil { + return err + } + } + } + + for issueID := range issueIDs { + if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?", + issueID, CommentTypeComment, issueID); err != nil { + return err + } + } + return committer.Commit() +} diff --git a/models/issues/comment_test.go b/models/issues/comment_test.go index d766625be3..90db476571 100644 --- a/models/issues/comment_test.go +++ b/models/issues/comment_test.go @@ -70,3 +70,30 @@ func TestAsCommentType(t *testing.T) { assert.Equal(t, issues_model.CommentTypeComment, issues_model.AsCommentType("comment")) assert.Equal(t, issues_model.CommentTypePRUnScheduledToAutoMerge, issues_model.AsCommentType("pull_cancel_scheduled_merge")) } + +func TestMigrate_InsertIssueComments(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) + _ = issue.LoadRepo(db.DefaultContext) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}) + reaction := &issues_model.Reaction{ + Type: "heart", + UserID: owner.ID, + } + + comment := &issues_model.Comment{ + PosterID: owner.ID, + Poster: owner, + IssueID: issue.ID, + Issue: issue, + Reactions: []*issues_model.Reaction{reaction}, + } + + err := issues_model.InsertIssueComments([]*issues_model.Comment{comment}) + assert.NoError(t, err) + + issueModified := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) + assert.EqualValues(t, issue.NumComments+1, issueModified.NumComments) + + unittest.CheckConsistencyFor(t, &issues_model.Issue{}) +} diff --git a/models/issues/issue.go b/models/issues/issue.go index 8f59c9cb42..341ec8547a 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -892,3 +892,50 @@ func IsNewPinAllowed(ctx context.Context, repoID int64, isPull bool) (bool, erro func IsErrIssueMaxPinReached(err error) bool { return err == ErrIssueMaxPinReached } + +// InsertIssues insert issues to database +func InsertIssues(issues ...*Issue) error { + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + + for _, issue := range issues { + if err := insertIssue(ctx, issue); err != nil { + return err + } + } + return committer.Commit() +} + +func insertIssue(ctx context.Context, issue *Issue) error { + sess := db.GetEngine(ctx) + if _, err := sess.NoAutoTime().Insert(issue); err != nil { + return err + } + issueLabels := make([]IssueLabel, 0, len(issue.Labels)) + for _, label := range issue.Labels { + issueLabels = append(issueLabels, IssueLabel{ + IssueID: issue.ID, + LabelID: label.ID, + }) + } + if len(issueLabels) > 0 { + if _, err := sess.Insert(issueLabels); err != nil { + return err + } + } + + for _, reaction := range issue.Reactions { + reaction.IssueID = issue.ID + } + + if len(issue.Reactions) > 0 { + if _, err := sess.Insert(issue.Reactions); err != nil { + return err + } + } + + return nil +} diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go index 0f2ceadc6b..f1bccc0cf8 100644 --- a/models/issues/issue_test.go +++ b/models/issues/issue_test.go @@ -573,3 +573,45 @@ func TestIssueLoadAttributes(t *testing.T) { } } } + +func assertCreateIssues(t *testing.T, isPull bool) { + assert.NoError(t, unittest.PrepareTestDatabase()) + reponame := "repo1" + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}) + milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}) + assert.EqualValues(t, milestone.ID, 1) + reaction := &issues_model.Reaction{ + Type: "heart", + UserID: owner.ID, + } + + title := "issuetitle1" + is := &issues_model.Issue{ + RepoID: repo.ID, + MilestoneID: milestone.ID, + Repo: repo, + Title: title, + Content: "issuecontent1", + IsPull: isPull, + PosterID: owner.ID, + Poster: owner, + IsClosed: true, + Labels: []*issues_model.Label{label}, + Reactions: []*issues_model.Reaction{reaction}, + } + err := issues_model.InsertIssues(is) + assert.NoError(t, err) + + i := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: title}) + unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: owner.ID, IssueID: i.ID}) +} + +func TestMigrate_CreateIssuesIsPullFalse(t *testing.T) { + assertCreateIssues(t, false) +} + +func TestMigrate_CreateIssuesIsPullTrue(t *testing.T) { + assertCreateIssues(t, true) +} diff --git a/models/issues/milestone.go b/models/issues/milestone.go index 1418e0869d..c15b2a41fe 100644 --- a/models/issues/milestone.go +++ b/models/issues/milestone.go @@ -10,7 +10,6 @@ import ( "code.gitea.io/gitea/models/db" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" @@ -323,261 +322,6 @@ func DeleteMilestoneByRepoID(repoID, id int64) error { return committer.Commit() } -// MilestoneList is a list of milestones offering additional functionality -type MilestoneList []*Milestone - -func (milestones MilestoneList) getMilestoneIDs() []int64 { - ids := make([]int64, 0, len(milestones)) - for _, ms := range milestones { - ids = append(ids, ms.ID) - } - return ids -} - -// GetMilestonesOption contain options to get milestones -type GetMilestonesOption struct { - db.ListOptions - RepoID int64 - State api.StateType - Name string - SortType string -} - -func (opts GetMilestonesOption) toCond() builder.Cond { - cond := builder.NewCond() - if opts.RepoID != 0 { - cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) - } - - switch opts.State { - case api.StateClosed: - cond = cond.And(builder.Eq{"is_closed": true}) - case api.StateAll: - break - // api.StateOpen: - default: - cond = cond.And(builder.Eq{"is_closed": false}) - } - - if len(opts.Name) != 0 { - cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name)) - } - - return cond -} - -// GetMilestones returns milestones filtered by GetMilestonesOption's -func GetMilestones(opts GetMilestonesOption) (MilestoneList, int64, error) { - sess := db.GetEngine(db.DefaultContext).Where(opts.toCond()) - - if opts.Page != 0 { - sess = db.SetSessionPagination(sess, &opts) - } - - switch opts.SortType { - case "furthestduedate": - sess.Desc("deadline_unix") - case "leastcomplete": - sess.Asc("completeness") - case "mostcomplete": - sess.Desc("completeness") - case "leastissues": - sess.Asc("num_issues") - case "mostissues": - sess.Desc("num_issues") - case "id": - sess.Asc("id") - default: - sess.Asc("deadline_unix").Asc("id") - } - - miles := make([]*Milestone, 0, opts.PageSize) - total, err := sess.FindAndCount(&miles) - return miles, total, err -} - -// GetMilestoneIDsByNames returns a list of milestone ids by given names. -// It doesn't filter them by repo, so it could return milestones belonging to different repos. -// It's used for filtering issues via indexer, otherwise it would be useless. -// Since it could return milestones with the same name, so the length of returned ids could be more than the length of names. -func GetMilestoneIDsByNames(ctx context.Context, names []string) ([]int64, error) { - var ids []int64 - return ids, db.GetEngine(ctx).Table("milestone"). - Where(db.BuildCaseInsensitiveIn("name", names)). - Cols("id"). - Find(&ids) -} - -// SearchMilestones search milestones -func SearchMilestones(repoCond builder.Cond, page int, isClosed bool, sortType, keyword string) (MilestoneList, error) { - miles := make([]*Milestone, 0, setting.UI.IssuePagingNum) - sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed) - if len(keyword) > 0 { - sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) - } - if repoCond.IsValid() { - sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) - } - if page > 0 { - sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum) - } - - switch sortType { - case "furthestduedate": - sess.Desc("deadline_unix") - case "leastcomplete": - sess.Asc("completeness") - case "mostcomplete": - sess.Desc("completeness") - case "leastissues": - sess.Asc("num_issues") - case "mostissues": - sess.Desc("num_issues") - default: - sess.Asc("deadline_unix") - } - return miles, sess.Find(&miles) -} - -// GetMilestonesByRepoIDs returns a list of milestones of given repositories and status. -func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) { - return SearchMilestones( - builder.In("repo_id", repoIDs), - page, - isClosed, - sortType, - "", - ) -} - -// MilestonesStats represents milestone statistic information. -type MilestonesStats struct { - OpenCount, ClosedCount int64 -} - -// Total returns the total counts of milestones -func (m MilestonesStats) Total() int64 { - return m.OpenCount + m.ClosedCount -} - -// GetMilestonesStatsByRepoCond returns milestone statistic information for dashboard by given conditions. -func GetMilestonesStatsByRepoCond(repoCond builder.Cond) (*MilestonesStats, error) { - var err error - stats := &MilestonesStats{} - - sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", false) - if repoCond.IsValid() { - sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) - } - stats.OpenCount, err = sess.Count(new(Milestone)) - if err != nil { - return nil, err - } - - sess = db.GetEngine(db.DefaultContext).Where("is_closed = ?", true) - if repoCond.IsValid() { - sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) - } - stats.ClosedCount, err = sess.Count(new(Milestone)) - if err != nil { - return nil, err - } - - return stats, nil -} - -// GetMilestonesStatsByRepoCondAndKw returns milestone statistic information for dashboard by given repo conditions and name keyword. -func GetMilestonesStatsByRepoCondAndKw(repoCond builder.Cond, keyword string) (*MilestonesStats, error) { - var err error - stats := &MilestonesStats{} - - sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", false) - if len(keyword) > 0 { - sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) - } - if repoCond.IsValid() { - sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) - } - stats.OpenCount, err = sess.Count(new(Milestone)) - if err != nil { - return nil, err - } - - sess = db.GetEngine(db.DefaultContext).Where("is_closed = ?", true) - if len(keyword) > 0 { - sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) - } - if repoCond.IsValid() { - sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) - } - stats.ClosedCount, err = sess.Count(new(Milestone)) - if err != nil { - return nil, err - } - - return stats, nil -} - -// 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(opts.toCond()). - Count(new(Milestone)) -} - -// 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) - if repoCond.IsValid() { - sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) - } - - countsSlice := make([]*struct { - RepoID int64 - Count int64 - }, 0, 10) - if err := sess.GroupBy("repo_id"). - Select("repo_id AS repo_id, COUNT(*) AS count"). - Table("milestone"). - Find(&countsSlice); err != nil { - return nil, err - } - - countMap := make(map[int64]int64, len(countsSlice)) - for _, c := range countsSlice { - countMap[c.RepoID] = c.Count - } - return countMap, nil -} - -// CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options` -func CountMilestonesByRepoCondAndKw(repoCond builder.Cond, keyword string, isClosed bool) (map[int64]int64, error) { - sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed) - if len(keyword) > 0 { - sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) - } - if repoCond.IsValid() { - sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) - } - - countsSlice := make([]*struct { - RepoID int64 - Count int64 - }, 0, 10) - if err := sess.GroupBy("repo_id"). - Select("repo_id AS repo_id, COUNT(*) AS count"). - Table("milestone"). - Find(&countsSlice); err != nil { - return nil, err - } - - countMap := make(map[int64]int64, len(countsSlice)) - for _, c := range countsSlice { - countMap[c.RepoID] = c.Count - } - return countMap, nil -} - func updateRepoMilestoneNum(ctx context.Context, repoID int64) error { _, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET num_milestones=(SELECT count(*) FROM milestone WHERE repo_id=?),num_closed_milestones=(SELECT count(*) FROM milestone WHERE repo_id=? AND is_closed=?) WHERE id=?", repoID, @@ -588,53 +332,6 @@ func updateRepoMilestoneNum(ctx context.Context, repoID int64) error { return err } -// _____ _ _ _____ _ -// |_ _| __ __ _ ___| | _____ __| |_ _(_)_ __ ___ ___ ___ -// | || '__/ _` |/ __| |/ / _ \/ _` | | | | | '_ ` _ \ / _ \/ __| -// | || | | (_| | (__| < __/ (_| | | | | | | | | | | __/\__ \ -// |_||_| \__,_|\___|_|\_\___|\__,_| |_| |_|_| |_| |_|\___||___/ -// - -func (milestones MilestoneList) loadTotalTrackedTimes(ctx context.Context) error { - type totalTimesByMilestone struct { - MilestoneID int64 - Time int64 - } - if len(milestones) == 0 { - return nil - } - trackedTimes := make(map[int64]int64, len(milestones)) - - // Get total tracked time by milestone_id - rows, err := db.GetEngine(ctx).Table("issue"). - Join("INNER", "milestone", "issue.milestone_id = milestone.id"). - Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id"). - Where("tracked_time.deleted = ?", false). - Select("milestone_id, sum(time) as time"). - In("milestone_id", milestones.getMilestoneIDs()). - GroupBy("milestone_id"). - Rows(new(totalTimesByMilestone)) - if err != nil { - return err - } - - defer rows.Close() - - for rows.Next() { - var totalTime totalTimesByMilestone - err = rows.Scan(&totalTime) - if err != nil { - return err - } - trackedTimes[totalTime.MilestoneID] = totalTime.Time - } - - for _, milestone := range milestones { - milestone.TotalTrackedTime = trackedTimes[milestone.ID] - } - return nil -} - func (m *Milestone) loadTotalTrackedTime(ctx context.Context) error { type totalTimesByMilestone struct { MilestoneID int64 @@ -658,12 +355,33 @@ func (m *Milestone) loadTotalTrackedTime(ctx context.Context) error { return nil } -// LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request -func (milestones MilestoneList) LoadTotalTrackedTimes() error { - return milestones.loadTotalTrackedTimes(db.DefaultContext) -} - // LoadTotalTrackedTime loads the tracked time for the milestone func (m *Milestone) LoadTotalTrackedTime() error { return m.loadTotalTrackedTime(db.DefaultContext) } + +// InsertMilestones creates milestones of repository. +func InsertMilestones(ms ...*Milestone) (err error) { + if len(ms) == 0 { + return nil + } + + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + sess := db.GetEngine(ctx) + + // to return the id, so we should not use batch insert + for _, m := range ms { + if _, err = sess.NoAutoTime().Insert(m); err != nil { + return err + } + } + + if _, err = db.Exec(ctx, "UPDATE `repository` SET num_milestones = num_milestones + ? WHERE id = ?", len(ms), ms[0].RepoID); err != nil { + return err + } + return committer.Commit() +} diff --git a/models/issues/milestone_list.go b/models/issues/milestone_list.go new file mode 100644 index 0000000000..b0c29106a0 --- /dev/null +++ b/models/issues/milestone_list.go @@ -0,0 +1,315 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issues + +import ( + "context" + "strings" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + + "xorm.io/builder" +) + +// MilestoneList is a list of milestones offering additional functionality +type MilestoneList []*Milestone + +func (milestones MilestoneList) getMilestoneIDs() []int64 { + ids := make([]int64, 0, len(milestones)) + for _, ms := range milestones { + ids = append(ids, ms.ID) + } + return ids +} + +// GetMilestonesOption contain options to get milestones +type GetMilestonesOption struct { + db.ListOptions + RepoID int64 + State api.StateType + Name string + SortType string +} + +func (opts GetMilestonesOption) toCond() builder.Cond { + cond := builder.NewCond() + if opts.RepoID != 0 { + cond = cond.And(builder.Eq{"repo_id": opts.RepoID}) + } + + switch opts.State { + case api.StateClosed: + cond = cond.And(builder.Eq{"is_closed": true}) + case api.StateAll: + break + // api.StateOpen: + default: + cond = cond.And(builder.Eq{"is_closed": false}) + } + + if len(opts.Name) != 0 { + cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name)) + } + + return cond +} + +// GetMilestones returns milestones filtered by GetMilestonesOption's +func GetMilestones(opts GetMilestonesOption) (MilestoneList, int64, error) { + sess := db.GetEngine(db.DefaultContext).Where(opts.toCond()) + + if opts.Page != 0 { + sess = db.SetSessionPagination(sess, &opts) + } + + switch opts.SortType { + case "furthestduedate": + sess.Desc("deadline_unix") + case "leastcomplete": + sess.Asc("completeness") + case "mostcomplete": + sess.Desc("completeness") + case "leastissues": + sess.Asc("num_issues") + case "mostissues": + sess.Desc("num_issues") + case "id": + sess.Asc("id") + default: + sess.Asc("deadline_unix").Asc("id") + } + + miles := make([]*Milestone, 0, opts.PageSize) + total, err := sess.FindAndCount(&miles) + return miles, total, err +} + +// GetMilestoneIDsByNames returns a list of milestone ids by given names. +// It doesn't filter them by repo, so it could return milestones belonging to different repos. +// It's used for filtering issues via indexer, otherwise it would be useless. +// Since it could return milestones with the same name, so the length of returned ids could be more than the length of names. +func GetMilestoneIDsByNames(ctx context.Context, names []string) ([]int64, error) { + var ids []int64 + return ids, db.GetEngine(ctx).Table("milestone"). + Where(db.BuildCaseInsensitiveIn("name", names)). + Cols("id"). + Find(&ids) +} + +// SearchMilestones search milestones +func SearchMilestones(repoCond builder.Cond, page int, isClosed bool, sortType, keyword string) (MilestoneList, error) { + miles := make([]*Milestone, 0, setting.UI.IssuePagingNum) + sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed) + if len(keyword) > 0 { + sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) + } + if repoCond.IsValid() { + sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) + } + if page > 0 { + sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum) + } + + switch sortType { + case "furthestduedate": + sess.Desc("deadline_unix") + case "leastcomplete": + sess.Asc("completeness") + case "mostcomplete": + sess.Desc("completeness") + case "leastissues": + sess.Asc("num_issues") + case "mostissues": + sess.Desc("num_issues") + default: + sess.Asc("deadline_unix") + } + return miles, sess.Find(&miles) +} + +// GetMilestonesByRepoIDs returns a list of milestones of given repositories and status. +func GetMilestonesByRepoIDs(repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) { + return SearchMilestones( + builder.In("repo_id", repoIDs), + page, + isClosed, + sortType, + "", + ) +} + +func (milestones MilestoneList) loadTotalTrackedTimes(ctx context.Context) error { + type totalTimesByMilestone struct { + MilestoneID int64 + Time int64 + } + if len(milestones) == 0 { + return nil + } + trackedTimes := make(map[int64]int64, len(milestones)) + + // Get total tracked time by milestone_id + rows, err := db.GetEngine(ctx).Table("issue"). + Join("INNER", "milestone", "issue.milestone_id = milestone.id"). + Join("LEFT", "tracked_time", "tracked_time.issue_id = issue.id"). + Where("tracked_time.deleted = ?", false). + Select("milestone_id, sum(time) as time"). + In("milestone_id", milestones.getMilestoneIDs()). + GroupBy("milestone_id"). + Rows(new(totalTimesByMilestone)) + if err != nil { + return err + } + + defer rows.Close() + + for rows.Next() { + var totalTime totalTimesByMilestone + err = rows.Scan(&totalTime) + if err != nil { + return err + } + trackedTimes[totalTime.MilestoneID] = totalTime.Time + } + + for _, milestone := range milestones { + milestone.TotalTrackedTime = trackedTimes[milestone.ID] + } + return nil +} + +// LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request +func (milestones MilestoneList) LoadTotalTrackedTimes() error { + return milestones.loadTotalTrackedTimes(db.DefaultContext) +} + +// 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(opts.toCond()). + Count(new(Milestone)) +} + +// 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) + if repoCond.IsValid() { + sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) + } + + countsSlice := make([]*struct { + RepoID int64 + Count int64 + }, 0, 10) + if err := sess.GroupBy("repo_id"). + Select("repo_id AS repo_id, COUNT(*) AS count"). + Table("milestone"). + Find(&countsSlice); err != nil { + return nil, err + } + + countMap := make(map[int64]int64, len(countsSlice)) + for _, c := range countsSlice { + countMap[c.RepoID] = c.Count + } + return countMap, nil +} + +// CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options` +func CountMilestonesByRepoCondAndKw(repoCond builder.Cond, keyword string, isClosed bool) (map[int64]int64, error) { + sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", isClosed) + if len(keyword) > 0 { + sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) + } + if repoCond.IsValid() { + sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond)) + } + + countsSlice := make([]*struct { + RepoID int64 + Count int64 + }, 0, 10) + if err := sess.GroupBy("repo_id"). + Select("repo_id AS repo_id, COUNT(*) AS count"). + Table("milestone"). + Find(&countsSlice); err != nil { + return nil, err + } + + countMap := make(map[int64]int64, len(countsSlice)) + for _, c := range countsSlice { + countMap[c.RepoID] = c.Count + } + return countMap, nil +} + +// MilestonesStats represents milestone statistic information. +type MilestonesStats struct { + OpenCount, ClosedCount int64 +} + +// Total returns the total counts of milestones +func (m MilestonesStats) Total() int64 { + return m.OpenCount + m.ClosedCount +} + +// GetMilestonesStatsByRepoCond returns milestone statistic information for dashboard by given conditions. +func GetMilestonesStatsByRepoCond(repoCond builder.Cond) (*MilestonesStats, error) { + var err error + stats := &MilestonesStats{} + + sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", false) + if repoCond.IsValid() { + sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) + } + stats.OpenCount, err = sess.Count(new(Milestone)) + if err != nil { + return nil, err + } + + sess = db.GetEngine(db.DefaultContext).Where("is_closed = ?", true) + if repoCond.IsValid() { + sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) + } + stats.ClosedCount, err = sess.Count(new(Milestone)) + if err != nil { + return nil, err + } + + return stats, nil +} + +// GetMilestonesStatsByRepoCondAndKw returns milestone statistic information for dashboard by given repo conditions and name keyword. +func GetMilestonesStatsByRepoCondAndKw(repoCond builder.Cond, keyword string) (*MilestonesStats, error) { + var err error + stats := &MilestonesStats{} + + sess := db.GetEngine(db.DefaultContext).Where("is_closed = ?", false) + if len(keyword) > 0 { + sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) + } + if repoCond.IsValid() { + sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) + } + stats.OpenCount, err = sess.Count(new(Milestone)) + if err != nil { + return nil, err + } + + sess = db.GetEngine(db.DefaultContext).Where("is_closed = ?", true) + if len(keyword) > 0 { + sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)}) + } + if repoCond.IsValid() { + sess.And(builder.In("repo_id", builder.Select("id").From("repository").Where(repoCond))) + } + stats.ClosedCount, err = sess.Count(new(Milestone)) + if err != nil { + return nil, err + } + + return stats, nil +} diff --git a/models/issues/milestone_test.go b/models/issues/milestone_test.go index 5db5655906..e85d77ebc8 100644 --- a/models/issues/milestone_test.go +++ b/models/issues/milestone_test.go @@ -351,3 +351,21 @@ func TestUpdateMilestoneCounters(t *testing.T) { assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID)) unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) } + +func TestMigrate_InsertMilestones(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + reponame := "repo1" + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) + name := "milestonetest1" + ms := &issues_model.Milestone{ + RepoID: repo.ID, + Name: name, + } + err := issues_model.InsertMilestones(ms) + assert.NoError(t, err) + unittest.AssertExistsAndLoadBean(t, ms) + repoModified := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) + assert.EqualValues(t, repo.NumMilestones+1, repoModified.NumMilestones) + + unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) +} diff --git a/models/issues/pull.go b/models/issues/pull.go index 676224a3d6..1c163ecca4 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -1105,3 +1105,23 @@ func TokenizeCodeOwnersLine(line string) []string { return tokens } + +// InsertPullRequests inserted pull requests +func InsertPullRequests(ctx context.Context, prs ...*PullRequest) error { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + sess := db.GetEngine(ctx) + for _, pr := range prs { + if err := insertIssue(ctx, pr.Issue); err != nil { + return err + } + pr.IssueID = pr.Issue.ID + if _, err := sess.NoAutoTime().Insert(pr); err != nil { + return err + } + } + return committer.Commit() +} diff --git a/models/issues/pull_test.go b/models/issues/pull_test.go index fa1f551adb..83977560ae 100644 --- a/models/issues/pull_test.go +++ b/models/issues/pull_test.go @@ -8,6 +8,7 @@ import ( "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/setting" @@ -337,3 +338,31 @@ func TestGetApprovers(t *testing.T) { expected := "Reviewed-by: User Five \nReviewed-by: User Six \n" assert.EqualValues(t, expected, approvers) } + +func TestMigrate_InsertPullRequests(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + reponame := "repo1" + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) + owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) + + i := &issues_model.Issue{ + RepoID: repo.ID, + Repo: repo, + Title: "title1", + Content: "issuecontent1", + IsPull: true, + PosterID: owner.ID, + Poster: owner, + } + + p := &issues_model.PullRequest{ + Issue: i, + } + + err := issues_model.InsertPullRequests(db.DefaultContext, p) + assert.NoError(t, err) + + _ = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: i.ID}) + + unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.PullRequest{}) +} diff --git a/models/migrate.go b/models/migrate.go deleted file mode 100644 index 9705d0ad04..0000000000 --- a/models/migrate.go +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package models - -import ( - "context" - - "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/modules/container" - "code.gitea.io/gitea/modules/structs" -) - -// InsertMilestones creates milestones of repository. -func InsertMilestones(ms ...*issues_model.Milestone) (err error) { - if len(ms) == 0 { - return nil - } - - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - sess := db.GetEngine(ctx) - - // to return the id, so we should not use batch insert - for _, m := range ms { - if _, err = sess.NoAutoTime().Insert(m); err != nil { - return err - } - } - - if _, err = db.Exec(ctx, "UPDATE `repository` SET num_milestones = num_milestones + ? WHERE id = ?", len(ms), ms[0].RepoID); err != nil { - return err - } - return committer.Commit() -} - -// InsertIssues insert issues to database -func InsertIssues(issues ...*issues_model.Issue) error { - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - - for _, issue := range issues { - if err := insertIssue(ctx, issue); err != nil { - return err - } - } - return committer.Commit() -} - -func insertIssue(ctx context.Context, issue *issues_model.Issue) error { - sess := db.GetEngine(ctx) - if _, err := sess.NoAutoTime().Insert(issue); err != nil { - return err - } - issueLabels := make([]issues_model.IssueLabel, 0, len(issue.Labels)) - for _, label := range issue.Labels { - issueLabels = append(issueLabels, issues_model.IssueLabel{ - IssueID: issue.ID, - LabelID: label.ID, - }) - } - if len(issueLabels) > 0 { - if _, err := sess.Insert(issueLabels); err != nil { - return err - } - } - - for _, reaction := range issue.Reactions { - reaction.IssueID = issue.ID - } - - if len(issue.Reactions) > 0 { - if _, err := sess.Insert(issue.Reactions); err != nil { - return err - } - } - - return nil -} - -// InsertIssueComments inserts many comments of issues. -func InsertIssueComments(comments []*issues_model.Comment) error { - if len(comments) == 0 { - return nil - } - - issueIDs := make(container.Set[int64]) - for _, comment := range comments { - issueIDs.Add(comment.IssueID) - } - - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - for _, comment := range comments { - if _, err := db.GetEngine(ctx).NoAutoTime().Insert(comment); err != nil { - return err - } - - for _, reaction := range comment.Reactions { - reaction.IssueID = comment.IssueID - reaction.CommentID = comment.ID - } - if len(comment.Reactions) > 0 { - if err := db.Insert(ctx, comment.Reactions); err != nil { - return err - } - } - } - - for issueID := range issueIDs { - if _, err := db.Exec(ctx, "UPDATE issue set num_comments = (SELECT count(*) FROM comment WHERE issue_id = ? AND `type`=?) WHERE id = ?", - issueID, issues_model.CommentTypeComment, issueID); err != nil { - return err - } - } - return committer.Commit() -} - -// InsertPullRequests inserted pull requests -func InsertPullRequests(ctx context.Context, prs ...*issues_model.PullRequest) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - sess := db.GetEngine(ctx) - for _, pr := range prs { - if err := insertIssue(ctx, pr.Issue); err != nil { - return err - } - pr.IssueID = pr.Issue.ID - if _, err := sess.NoAutoTime().Insert(pr); err != nil { - return err - } - } - return committer.Commit() -} - -// InsertReleases migrates release -func InsertReleases(rels ...*repo_model.Release) error { - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - sess := db.GetEngine(ctx) - - for _, rel := range rels { - if _, err := sess.NoAutoTime().Insert(rel); err != nil { - return err - } - - if len(rel.Attachments) > 0 { - for i := range rel.Attachments { - rel.Attachments[i].ReleaseID = rel.ID - } - - if _, err := sess.NoAutoTime().Insert(rel.Attachments); err != nil { - return err - } - } - } - - return committer.Commit() -} - -// UpdateMigrationsByType updates all migrated repositories' posterid from gitServiceType to replace originalAuthorID to posterID -func UpdateMigrationsByType(tp structs.GitServiceType, externalUserID string, userID int64) error { - if err := issues_model.UpdateIssuesMigrationsByType(tp, externalUserID, userID); err != nil { - return err - } - - if err := issues_model.UpdateCommentsMigrationsByType(tp, externalUserID, userID); err != nil { - return err - } - - if err := repo_model.UpdateReleasesMigrationsByType(tp, externalUserID, userID); err != nil { - return err - } - - if err := issues_model.UpdateReactionsMigrationsByType(tp, externalUserID, userID); err != nil { - return err - } - return issues_model.UpdateReviewsMigrationsByType(tp, externalUserID, userID) -} diff --git a/models/migrate_test.go b/models/migrate_test.go deleted file mode 100644 index 74736a2849..0000000000 --- a/models/migrate_test.go +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package models - -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" - - "github.com/stretchr/testify/assert" -) - -func TestMigrate_InsertMilestones(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - reponame := "repo1" - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) - name := "milestonetest1" - ms := &issues_model.Milestone{ - RepoID: repo.ID, - Name: name, - } - err := InsertMilestones(ms) - assert.NoError(t, err) - unittest.AssertExistsAndLoadBean(t, ms) - repoModified := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repo.ID}) - assert.EqualValues(t, repo.NumMilestones+1, repoModified.NumMilestones) - - unittest.CheckConsistencyFor(t, &issues_model.Milestone{}) -} - -func assertCreateIssues(t *testing.T, isPull bool) { - assert.NoError(t, unittest.PrepareTestDatabase()) - reponame := "repo1" - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1}) - milestone := unittest.AssertExistsAndLoadBean(t, &issues_model.Milestone{ID: 1}) - assert.EqualValues(t, milestone.ID, 1) - reaction := &issues_model.Reaction{ - Type: "heart", - UserID: owner.ID, - } - - title := "issuetitle1" - is := &issues_model.Issue{ - RepoID: repo.ID, - MilestoneID: milestone.ID, - Repo: repo, - Title: title, - Content: "issuecontent1", - IsPull: isPull, - PosterID: owner.ID, - Poster: owner, - IsClosed: true, - Labels: []*issues_model.Label{label}, - Reactions: []*issues_model.Reaction{reaction}, - } - err := InsertIssues(is) - assert.NoError(t, err) - - i := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: title}) - unittest.AssertExistsAndLoadBean(t, &issues_model.Reaction{Type: "heart", UserID: owner.ID, IssueID: i.ID}) -} - -func TestMigrate_CreateIssuesIsPullFalse(t *testing.T) { - assertCreateIssues(t, false) -} - -func TestMigrate_CreateIssuesIsPullTrue(t *testing.T) { - assertCreateIssues(t, true) -} - -func TestMigrate_InsertIssueComments(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) - _ = issue.LoadRepo(db.DefaultContext) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: issue.Repo.OwnerID}) - reaction := &issues_model.Reaction{ - Type: "heart", - UserID: owner.ID, - } - - comment := &issues_model.Comment{ - PosterID: owner.ID, - Poster: owner, - IssueID: issue.ID, - Issue: issue, - Reactions: []*issues_model.Reaction{reaction}, - } - - err := InsertIssueComments([]*issues_model.Comment{comment}) - assert.NoError(t, err) - - issueModified := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1}) - assert.EqualValues(t, issue.NumComments+1, issueModified.NumComments) - - unittest.CheckConsistencyFor(t, &issues_model.Issue{}) -} - -func TestMigrate_InsertPullRequests(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - reponame := "repo1" - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: reponame}) - owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) - - i := &issues_model.Issue{ - RepoID: repo.ID, - Repo: repo, - Title: "title1", - Content: "issuecontent1", - IsPull: true, - PosterID: owner.ID, - Poster: owner, - } - - p := &issues_model.PullRequest{ - Issue: i, - } - - err := InsertPullRequests(db.DefaultContext, p) - assert.NoError(t, err) - - _ = unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{IssueID: i.ID}) - - unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.PullRequest{}) -} - -func TestMigrate_InsertReleases(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - a := &repo_model.Attachment{ - UUID: "a0eebc91-9c0c-4ef7-bb6e-6bb9bd380a12", - } - r := &repo_model.Release{ - Attachments: []*repo_model.Attachment{a}, - } - - err := InsertReleases(r) - assert.NoError(t, err) -} diff --git a/models/org_team.go b/models/org_team.go index f887c9ee98..cf3680990d 100644 --- a/models/org_team.go +++ b/models/org_team.go @@ -485,12 +485,12 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64 } // Remove watches from now unaccessible - if err := reconsiderWatches(ctx, repo, userID); err != nil { + if err := ReconsiderWatches(ctx, repo, userID); err != nil { return err } // Remove issue assignments from now unaccessible - if err := reconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil { + if err := ReconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil { return err } } @@ -523,3 +523,33 @@ func RemoveTeamMember(team *organization.Team, userID int64) error { } return committer.Commit() } + +func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, uid int64) error { + user, err := user_model.GetUserByID(ctx, uid) + if err != nil { + return err + } + + if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned { + return err + } + + if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": uid}). + In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})). + Delete(&issues_model.IssueAssignees{}); err != nil { + return fmt.Errorf("Could not delete assignee[%d] %w", uid, err) + } + return nil +} + +func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, uid int64) error { + if has, err := access_model.HasAccess(ctx, uid, repo); err != nil || has { + return err + } + if err := repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil { + return err + } + + // Remove all IssueWatches a user has subscribed to in the repository + return issues_model.RemoveIssueWatchersByRepoID(ctx, uid, repo.ID) +} diff --git a/models/repo/release.go b/models/repo/release.go index 191475d541..0e92474365 100644 --- a/models/repo/release.go +++ b/models/repo/release.go @@ -552,3 +552,31 @@ func (r *Release) GetExternalName() string { return r.OriginalAuthor } // ExternalID ExternalUserRemappable interface func (r *Release) GetExternalID() int64 { return r.OriginalAuthorID } + +// InsertReleases migrates release +func InsertReleases(rels ...*Release) error { + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + sess := db.GetEngine(ctx) + + for _, rel := range rels { + if _, err := sess.NoAutoTime().Insert(rel); err != nil { + return err + } + + if len(rel.Attachments) > 0 { + for i := range rel.Attachments { + rel.Attachments[i].ReleaseID = rel.ID + } + + if _, err := sess.NoAutoTime().Insert(rel.Attachments); err != nil { + return err + } + } + } + + return committer.Commit() +} diff --git a/models/repo/release_test.go b/models/repo/release_test.go new file mode 100644 index 0000000000..2a45ab32f3 --- /dev/null +++ b/models/repo/release_test.go @@ -0,0 +1,26 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "testing" + + "code.gitea.io/gitea/models/unittest" + + "github.com/stretchr/testify/assert" +) + +func TestMigrate_InsertReleases(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + a := &Attachment{ + UUID: "a0eebc91-9c0c-4ef7-bb6e-6bb9bd380a12", + } + r := &Release{ + Attachments: []*Attachment{a}, + } + + err := InsertReleases(r) + assert.NoError(t, err) +} diff --git a/models/repo_collaboration.go b/models/repo_collaboration.go deleted file mode 100644 index b85880eab9..0000000000 --- a/models/repo_collaboration.go +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2016 The Gogs Authors. All rights reserved. -// Copyright 2020 The Gitea Authors. -// SPDX-License-Identifier: MIT - -package models - -import ( - "context" - "fmt" - - "code.gitea.io/gitea/models/db" - issues_model "code.gitea.io/gitea/models/issues" - 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" - - "xorm.io/builder" -) - -// DeleteCollaboration removes collaboration relation between the user and repository. -func DeleteCollaboration(repo *repo_model.Repository, uid int64) (err error) { - collaboration := &repo_model.Collaboration{ - RepoID: repo.ID, - UserID: uid, - } - - ctx, committer, err := db.TxContext(db.DefaultContext) - if err != nil { - return err - } - defer committer.Close() - - if has, err := db.GetEngine(ctx).Delete(collaboration); err != nil || has == 0 { - return err - } else if err = access_model.RecalculateAccesses(ctx, repo); err != nil { - return err - } - - if err = repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil { - return err - } - - if err = reconsiderWatches(ctx, repo, uid); err != nil { - return err - } - - // Unassign a user from any issue (s)he has been assigned to in the repository - if err := reconsiderRepoIssuesAssignee(ctx, repo, uid); err != nil { - return err - } - - return committer.Commit() -} - -func reconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, uid int64) error { - user, err := user_model.GetUserByID(ctx, uid) - if err != nil { - return err - } - - if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned { - return err - } - - if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": uid}). - In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})). - Delete(&issues_model.IssueAssignees{}); err != nil { - return fmt.Errorf("Could not delete assignee[%d] %w", uid, err) - } - return nil -} - -func reconsiderWatches(ctx context.Context, repo *repo_model.Repository, uid int64) error { - if has, err := access_model.HasAccess(ctx, uid, repo); err != nil || has { - return err - } - if err := repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil { - return err - } - - // Remove all IssueWatches a user has subscribed to in the repository - return issues_model.RemoveIssueWatchersByRepoID(ctx, uid, repo.ID) -} diff --git a/routers/api/v1/repo/collaborators.go b/routers/api/v1/repo/collaborators.go index 942d4c799f..66e7577a4b 100644 --- a/routers/api/v1/repo/collaborators.go +++ b/routers/api/v1/repo/collaborators.go @@ -8,7 +8,6 @@ import ( "errors" "net/http" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" @@ -19,6 +18,7 @@ import ( "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/utils" "code.gitea.io/gitea/services/convert" + repo_service "code.gitea.io/gitea/services/repository" ) // ListCollaborators list a repository's collaborators @@ -228,7 +228,7 @@ func DeleteCollaborator(ctx *context.APIContext) { return } - if err := models.DeleteCollaboration(ctx.Repo.Repository, collaborator.ID); err != nil { + if err := repo_service.DeleteCollaboration(ctx.Repo.Repository, collaborator.ID); err != nil { ctx.Error(http.StatusInternalServerError, "DeleteCollaboration", err) return } diff --git a/routers/web/repo/setting/collaboration.go b/routers/web/repo/setting/collaboration.go index 84e5fd30ca..212b0346bc 100644 --- a/routers/web/repo/setting/collaboration.go +++ b/routers/web/repo/setting/collaboration.go @@ -7,7 +7,6 @@ import ( "net/http" "strings" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" @@ -128,7 +127,7 @@ func ChangeCollaborationAccessMode(ctx *context.Context) { // DeleteCollaboration delete a collaboration for a repository func DeleteCollaboration(ctx *context.Context) { - if err := models.DeleteCollaboration(ctx.Repo.Repository, ctx.FormInt64("id")); err != nil { + if err := repo_service.DeleteCollaboration(ctx.Repo.Repository, ctx.FormInt64("id")); err != nil { ctx.Flash.Error("DeleteCollaboration: " + err.Error()) } else { ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success")) diff --git a/services/externalaccount/user.go b/services/externalaccount/user.go index 87d2e02b48..3da5af3486 100644 --- a/services/externalaccount/user.go +++ b/services/externalaccount/user.go @@ -6,8 +6,9 @@ package externalaccount import ( "strings" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/auth" + issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/structs" @@ -62,7 +63,7 @@ func LinkAccountToUser(user *user_model.User, gothUser goth.User) error { } if tp.Name() != "" { - return models.UpdateMigrationsByType(tp, externalID, user.ID) + return UpdateMigrationsByType(tp, externalID, user.ID) } return nil @@ -77,3 +78,23 @@ func UpdateExternalUser(user *user_model.User, gothUser goth.User) error { return user_model.UpdateExternalUserByExternalID(externalLoginUser) } + +// UpdateMigrationsByType updates all migrated repositories' posterid from gitServiceType to replace originalAuthorID to posterID +func UpdateMigrationsByType(tp structs.GitServiceType, externalUserID string, userID int64) error { + if err := issues_model.UpdateIssuesMigrationsByType(tp, externalUserID, userID); err != nil { + return err + } + + if err := issues_model.UpdateCommentsMigrationsByType(tp, externalUserID, userID); err != nil { + return err + } + + if err := repo_model.UpdateReleasesMigrationsByType(tp, externalUserID, userID); err != nil { + return err + } + + if err := issues_model.UpdateReactionsMigrationsByType(tp, externalUserID, userID); err != nil { + return err + } + return issues_model.UpdateReviewsMigrationsByType(tp, externalUserID, userID) +} diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index 3f055707ae..4c21efae44 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -205,7 +205,7 @@ func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) err mss = append(mss, &ms) } - err := models.InsertMilestones(mss...) + err := issues_model.InsertMilestones(mss...) if err != nil { return err } @@ -350,7 +350,7 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error { rels = append(rels, &rel) } - return models.InsertReleases(rels...) + return repo_model.InsertReleases(rels...) } // SyncTags syncs releases with tags in the database @@ -430,7 +430,7 @@ func (g *GiteaLocalUploader) CreateIssues(issues ...*base.Issue) error { } if len(iss) > 0 { - if err := models.InsertIssues(iss...); err != nil { + if err := issues_model.InsertIssues(iss...); err != nil { return err } @@ -510,7 +510,7 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error { if len(cms) == 0 { return nil } - return models.InsertIssueComments(cms) + return issues_model.InsertIssueComments(cms) } // CreatePullRequests creates pull requests @@ -529,7 +529,7 @@ func (g *GiteaLocalUploader) CreatePullRequests(prs ...*base.PullRequest) error gprs = append(gprs, gpr) } - if err := models.InsertPullRequests(ctx, gprs...); err != nil { + if err := issues_model.InsertPullRequests(ctx, gprs...); err != nil { return err } for _, pr := range gprs { diff --git a/services/migrations/update.go b/services/migrations/update.go index 48b61885e8..2adca01dfe 100644 --- a/services/migrations/update.go +++ b/services/migrations/update.go @@ -6,11 +6,11 @@ package migrations import ( "context" - "code.gitea.io/gitea/models" "code.gitea.io/gitea/models/db" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/services/externalaccount" ) // UpdateMigrationPosterID updates all migrated repositories' issues and comments posterID @@ -62,7 +62,7 @@ func updateMigrationPosterIDByGitService(ctx context.Context, tp structs.GitServ default: } externalUserID := user.ExternalID - if err := models.UpdateMigrationsByType(tp, externalUserID, user.UserID); err != nil { + if err := externalaccount.UpdateMigrationsByType(tp, externalUserID, user.UserID); err != nil { log.Error("UpdateMigrationsByType type %s external user id %v to local user id %v failed: %v", tp.Name(), user.ExternalID, user.UserID, err) } } diff --git a/services/repository/collaboration.go b/services/repository/collaboration.go new file mode 100644 index 0000000000..28824d83f5 --- /dev/null +++ b/services/repository/collaboration.go @@ -0,0 +1,47 @@ +// Copyright 2016 The Gogs Authors. All rights reserved. +// Copyright 2020 The Gitea Authors. +// SPDX-License-Identifier: MIT + +package repository + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" + access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" +) + +// DeleteCollaboration removes collaboration relation between the user and repository. +func DeleteCollaboration(repo *repo_model.Repository, uid int64) (err error) { + collaboration := &repo_model.Collaboration{ + RepoID: repo.ID, + UserID: uid, + } + + ctx, committer, err := db.TxContext(db.DefaultContext) + if err != nil { + return err + } + defer committer.Close() + + if has, err := db.GetEngine(ctx).Delete(collaboration); err != nil || has == 0 { + return err + } else if err = access_model.RecalculateAccesses(ctx, repo); err != nil { + return err + } + + if err = repo_model.WatchRepo(ctx, uid, repo.ID, false); err != nil { + return err + } + + if err = models.ReconsiderWatches(ctx, repo, uid); err != nil { + return err + } + + // Unassign a user from any issue (s)he has been assigned to in the repository + if err := models.ReconsiderRepoIssuesAssignee(ctx, repo, uid); err != nil { + return err + } + + return committer.Commit() +} diff --git a/models/repo_collaboration_test.go b/services/repository/collaboration_test.go similarity index 97% rename from models/repo_collaboration_test.go rename to services/repository/collaboration_test.go index 95fb35fe6d..08159af7bc 100644 --- a/models/repo_collaboration_test.go +++ b/services/repository/collaboration_test.go @@ -1,7 +1,7 @@ // Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package models +package repository import ( "testing"