mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 07:21:36 +01:00 
			
		
		
		
	Move some functions to service layer (#26969)
This commit is contained in:
		
							parent
							
								
									b8ad558c93
								
							
						
					
					
						commit
						e3ed67859a
					
				| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user