mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-20 20:07:03 +01:00
Move some functions to service layer (#26969)
This commit is contained in:
parent
b8ad558c93
commit
e3ed67859a
22 changed files with 733 additions and 748 deletions
|
@ -17,6 +17,7 @@ import (
|
||||||
project_model "code.gitea.io/gitea/models/project"
|
project_model "code.gitea.io/gitea/models/project"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
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/git"
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
@ -1247,3 +1248,44 @@ func FixCommentTypeLabelWithOutsideLabels(ctx context.Context) (int64, error) {
|
||||||
func (c *Comment) HasOriginalAuthor() bool {
|
func (c *Comment) HasOriginalAuthor() bool {
|
||||||
return c.OriginalAuthor != "" && c.OriginalAuthorID != 0
|
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()
|
||||||
|
}
|
||||||
|
|
|
@ -70,3 +70,30 @@ func TestAsCommentType(t *testing.T) {
|
||||||
assert.Equal(t, issues_model.CommentTypeComment, issues_model.AsCommentType("comment"))
|
assert.Equal(t, issues_model.CommentTypeComment, issues_model.AsCommentType("comment"))
|
||||||
assert.Equal(t, issues_model.CommentTypePRUnScheduledToAutoMerge, issues_model.AsCommentType("pull_cancel_scheduled_merge"))
|
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{})
|
||||||
|
}
|
||||||
|
|
|
@ -892,3 +892,50 @@ func IsNewPinAllowed(ctx context.Context, repoID int64, isPull bool) (bool, erro
|
||||||
func IsErrIssueMaxPinReached(err error) bool {
|
func IsErrIssueMaxPinReached(err error) bool {
|
||||||
return err == ErrIssueMaxPinReached
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
@ -323,261 +322,6 @@ func DeleteMilestoneByRepoID(repoID, id int64) error {
|
||||||
return committer.Commit()
|
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 {
|
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=?",
|
_, 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,
|
repoID,
|
||||||
|
@ -588,53 +332,6 @@ func updateRepoMilestoneNum(ctx context.Context, repoID int64) error {
|
||||||
return err
|
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 {
|
func (m *Milestone) loadTotalTrackedTime(ctx context.Context) error {
|
||||||
type totalTimesByMilestone struct {
|
type totalTimesByMilestone struct {
|
||||||
MilestoneID int64
|
MilestoneID int64
|
||||||
|
@ -658,12 +355,33 @@ func (m *Milestone) loadTotalTrackedTime(ctx context.Context) error {
|
||||||
return nil
|
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
|
// LoadTotalTrackedTime loads the tracked time for the milestone
|
||||||
func (m *Milestone) LoadTotalTrackedTime() error {
|
func (m *Milestone) LoadTotalTrackedTime() error {
|
||||||
return m.loadTotalTrackedTime(db.DefaultContext)
|
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()
|
||||||
|
}
|
||||||
|
|
315
models/issues/milestone_list.go
Normal file
315
models/issues/milestone_list.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -351,3 +351,21 @@ func TestUpdateMilestoneCounters(t *testing.T) {
|
||||||
assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID))
|
assert.NoError(t, issues_model.UpdateMilestoneCounters(db.DefaultContext, issue.MilestoneID))
|
||||||
unittest.CheckConsistencyFor(t, &issues_model.Milestone{})
|
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{})
|
||||||
|
}
|
||||||
|
|
|
@ -1105,3 +1105,23 @@ func TokenizeCodeOwnersLine(line string) []string {
|
||||||
|
|
||||||
return tokens
|
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()
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
@ -337,3 +338,31 @@ func TestGetApprovers(t *testing.T) {
|
||||||
expected := "Reviewed-by: User Five <user5@example.com>\nReviewed-by: User Six <user6@example.com>\n"
|
expected := "Reviewed-by: User Five <user5@example.com>\nReviewed-by: User Six <user6@example.com>\n"
|
||||||
assert.EqualValues(t, expected, approvers)
|
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{})
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -485,12 +485,12 @@ func removeTeamMember(ctx context.Context, team *organization.Team, userID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove watches from now unaccessible
|
// Remove watches from now unaccessible
|
||||||
if err := reconsiderWatches(ctx, repo, userID); err != nil {
|
if err := ReconsiderWatches(ctx, repo, userID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove issue assignments from now unaccessible
|
// Remove issue assignments from now unaccessible
|
||||||
if err := reconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil {
|
if err := ReconsiderRepoIssuesAssignee(ctx, repo, userID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -523,3 +523,33 @@ func RemoveTeamMember(team *organization.Team, userID int64) error {
|
||||||
}
|
}
|
||||||
return committer.Commit()
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -552,3 +552,31 @@ func (r *Release) GetExternalName() string { return r.OriginalAuthor }
|
||||||
|
|
||||||
// ExternalID ExternalUserRemappable interface
|
// ExternalID ExternalUserRemappable interface
|
||||||
func (r *Release) GetExternalID() int64 { return r.OriginalAuthorID }
|
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()
|
||||||
|
}
|
||||||
|
|
26
models/repo/release_test.go
Normal file
26
models/repo/release_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
@ -19,6 +18,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||||
"code.gitea.io/gitea/services/convert"
|
"code.gitea.io/gitea/services/convert"
|
||||||
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListCollaborators list a repository's collaborators
|
// ListCollaborators list a repository's collaborators
|
||||||
|
@ -228,7 +228,7 @@ func DeleteCollaborator(ctx *context.APIContext) {
|
||||||
return
|
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)
|
ctx.Error(http.StatusInternalServerError, "DeleteCollaboration", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/organization"
|
"code.gitea.io/gitea/models/organization"
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
|
@ -128,7 +127,7 @@ func ChangeCollaborationAccessMode(ctx *context.Context) {
|
||||||
|
|
||||||
// DeleteCollaboration delete a collaboration for a repository
|
// DeleteCollaboration delete a collaboration for a repository
|
||||||
func DeleteCollaboration(ctx *context.Context) {
|
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())
|
ctx.Flash.Error("DeleteCollaboration: " + err.Error())
|
||||||
} else {
|
} else {
|
||||||
ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
|
ctx.Flash.Success(ctx.Tr("repo.settings.remove_collaborator_success"))
|
||||||
|
|
|
@ -6,8 +6,9 @@ package externalaccount
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
|
||||||
"code.gitea.io/gitea/models/auth"
|
"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"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
|
|
||||||
|
@ -62,7 +63,7 @@ func LinkAccountToUser(user *user_model.User, gothUser goth.User) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if tp.Name() != "" {
|
if tp.Name() != "" {
|
||||||
return models.UpdateMigrationsByType(tp, externalID, user.ID)
|
return UpdateMigrationsByType(tp, externalID, user.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -77,3 +78,23 @@ func UpdateExternalUser(user *user_model.User, gothUser goth.User) error {
|
||||||
|
|
||||||
return user_model.UpdateExternalUserByExternalID(externalLoginUser)
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -205,7 +205,7 @@ func (g *GiteaLocalUploader) CreateMilestones(milestones ...*base.Milestone) err
|
||||||
mss = append(mss, &ms)
|
mss = append(mss, &ms)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := models.InsertMilestones(mss...)
|
err := issues_model.InsertMilestones(mss...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -350,7 +350,7 @@ func (g *GiteaLocalUploader) CreateReleases(releases ...*base.Release) error {
|
||||||
rels = append(rels, &rel)
|
rels = append(rels, &rel)
|
||||||
}
|
}
|
||||||
|
|
||||||
return models.InsertReleases(rels...)
|
return repo_model.InsertReleases(rels...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SyncTags syncs releases with tags in the database
|
// 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 len(iss) > 0 {
|
||||||
if err := models.InsertIssues(iss...); err != nil {
|
if err := issues_model.InsertIssues(iss...); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -510,7 +510,7 @@ func (g *GiteaLocalUploader) CreateComments(comments ...*base.Comment) error {
|
||||||
if len(cms) == 0 {
|
if len(cms) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return models.InsertIssueComments(cms)
|
return issues_model.InsertIssueComments(cms)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreatePullRequests creates pull requests
|
// CreatePullRequests creates pull requests
|
||||||
|
@ -529,7 +529,7 @@ func (g *GiteaLocalUploader) CreatePullRequests(prs ...*base.PullRequest) error
|
||||||
|
|
||||||
gprs = append(gprs, gpr)
|
gprs = append(gprs, gpr)
|
||||||
}
|
}
|
||||||
if err := models.InsertPullRequests(ctx, gprs...); err != nil {
|
if err := issues_model.InsertPullRequests(ctx, gprs...); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, pr := range gprs {
|
for _, pr := range gprs {
|
||||||
|
|
|
@ -6,11 +6,11 @@ package migrations
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/structs"
|
"code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/services/externalaccount"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpdateMigrationPosterID updates all migrated repositories' issues and comments posterID
|
// UpdateMigrationPosterID updates all migrated repositories' issues and comments posterID
|
||||||
|
@ -62,7 +62,7 @@ func updateMigrationPosterIDByGitService(ctx context.Context, tp structs.GitServ
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
externalUserID := user.ExternalID
|
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)
|
log.Error("UpdateMigrationsByType type %s external user id %v to local user id %v failed: %v", tp.Name(), user.ExternalID, user.UserID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
47
services/repository/collaboration.go
Normal file
47
services/repository/collaboration.go
Normal file
|
@ -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()
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package models
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
Loading…
Reference in a new issue